@headless-lms/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/events.d.ts +18 -18
- package/dist/assets/file-urls.d.ts +3 -0
- package/dist/assets/file-urls.js +1 -0
- package/dist/assets/file-urls.js.map +1 -1
- package/dist/assets/index.js +1 -1
- package/dist/assets/ports.d.ts +1 -1
- package/dist/assets/service.d.ts +1 -1
- package/dist/assets/service.js +28 -23
- package/dist/assets/service.js.map +1 -1
- package/dist/automations/catalog.js +1 -1
- package/dist/automations/notifications/processor.js +2 -2
- package/dist/automations/notifications/service.js +1 -1
- package/dist/automations/service.js +1 -1
- package/dist/content/events.d.ts +84 -48
- package/dist/content/index.d.ts +2 -2
- package/dist/content/model.d.ts +2 -2
- package/dist/content/notification-categories.js +1 -1
- package/dist/content/ports.d.ts +11 -11
- package/dist/content/service.d.ts +16 -12
- package/dist/content/service.js +62 -16
- package/dist/content/service.js.map +1 -1
- package/dist/discussion/chats.js +2 -2
- package/dist/discussion/events.d.ts +8 -8
- package/dist/discussion/posts.d.ts +1 -1
- package/dist/discussion/posts.js +3 -3
- package/dist/discussion/posts.js.map +1 -1
- package/dist/discussion/service.js +2 -2
- package/dist/discussion/subscriptions.js +1 -1
- package/dist/entitlements/events.d.ts +48 -48
- package/dist/entitlements/service.js +1 -1
- package/dist/identity/devices.js +1 -1
- package/dist/identity/events.d.ts +2 -2
- package/dist/identity/notification-settings.js +1 -1
- package/dist/identity/service.d.ts +1 -1
- package/dist/identity/service.js +3 -3
- package/dist/identity/service.js.map +1 -1
- package/dist/integrations/service.js +2 -2
- package/dist/organizations/effective-permissions.d.ts +3 -1
- package/dist/organizations/effective-permissions.js +8 -3
- package/dist/organizations/effective-permissions.js.map +1 -1
- package/dist/organizations/events.d.ts +29 -23
- package/dist/organizations/index.d.ts +2 -2
- package/dist/organizations/model.d.ts +2 -2
- package/dist/organizations/notification-categories.js +1 -1
- package/dist/organizations/ports.d.ts +6 -3
- package/dist/organizations/service.d.ts +1 -1
- package/dist/organizations/service.js +17 -7
- package/dist/organizations/service.js.map +1 -1
- package/dist/organizations/testing.js +1 -1
- package/dist/organizations/testing.js.map +1 -1
- package/dist/progress/service.js +2 -2
- package/dist/reporting/member/model.d.ts +3 -0
- package/dist/reporting/member/ports.d.ts +2 -1
- package/dist/reporting/member/service.d.ts +2 -1
- package/dist/reporting/member/service.js +17 -1
- package/dist/reporting/member/service.js.map +1 -1
- package/dist/shared/actor.d.ts +1 -0
- package/dist/types/content.d.ts +2 -2
- package/dist/types/index.d.ts +3 -3
- package/dist/types/organizations.d.ts +2 -2
- package/dist/types/schemas/content.d.ts +68 -6
- package/dist/types/schemas/content.js +19 -2
- package/dist/types/schemas/content.js.map +1 -1
- package/dist/types/schemas/discussion.d.ts +14 -14
- package/dist/types/schemas/entitlements.d.ts +8 -8
- package/dist/types/schemas/index.d.ts +3 -3
- package/dist/types/schemas/index.js +3 -3
- package/dist/types/schemas/notifications.d.ts +4 -4
- package/dist/types/schemas/organizations.d.ts +4 -1
- package/dist/types/schemas/organizations.js +3 -1
- package/dist/types/schemas/organizations.js.map +1 -1
- package/package.json +1 -1
package/dist/assets/events.d.ts
CHANGED
|
@@ -39,20 +39,20 @@ declare const assetEvents: {
|
|
|
39
39
|
audio: "audio";
|
|
40
40
|
document: "document";
|
|
41
41
|
}>;
|
|
42
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
43
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
44
|
-
status: import("zod").ZodEnum<{
|
|
45
|
-
pending: "pending";
|
|
46
|
-
ready: "ready";
|
|
47
|
-
}>;
|
|
48
42
|
source: import("zod").ZodEnum<{
|
|
49
43
|
library: "library";
|
|
50
44
|
member: "member";
|
|
51
45
|
}>;
|
|
46
|
+
status: import("zod").ZodEnum<{
|
|
47
|
+
pending: "pending";
|
|
48
|
+
ready: "ready";
|
|
49
|
+
}>;
|
|
52
50
|
uploadedBy: import("zod").ZodString;
|
|
53
51
|
filename: import("zod").ZodString;
|
|
54
52
|
contentType: import("zod").ZodString;
|
|
55
53
|
size: import("zod").ZodNumber;
|
|
54
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
55
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
56
56
|
}, import("zod/v4/core").$strip>>;
|
|
57
57
|
assetReady: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
58
58
|
orgId: string;
|
|
@@ -91,20 +91,20 @@ declare const assetEvents: {
|
|
|
91
91
|
audio: "audio";
|
|
92
92
|
document: "document";
|
|
93
93
|
}>;
|
|
94
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
95
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
96
|
-
status: import("zod").ZodEnum<{
|
|
97
|
-
pending: "pending";
|
|
98
|
-
ready: "ready";
|
|
99
|
-
}>;
|
|
100
94
|
source: import("zod").ZodEnum<{
|
|
101
95
|
library: "library";
|
|
102
96
|
member: "member";
|
|
103
97
|
}>;
|
|
98
|
+
status: import("zod").ZodEnum<{
|
|
99
|
+
pending: "pending";
|
|
100
|
+
ready: "ready";
|
|
101
|
+
}>;
|
|
104
102
|
uploadedBy: import("zod").ZodString;
|
|
105
103
|
filename: import("zod").ZodString;
|
|
106
104
|
contentType: import("zod").ZodString;
|
|
107
105
|
size: import("zod").ZodNumber;
|
|
106
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
107
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
108
108
|
}, import("zod/v4/core").$strip>>;
|
|
109
109
|
assetDeleted: RuntimeEventDefinition<VersionedDomainEvent<{
|
|
110
110
|
orgId: string;
|
|
@@ -143,20 +143,20 @@ declare const assetEvents: {
|
|
|
143
143
|
audio: "audio";
|
|
144
144
|
document: "document";
|
|
145
145
|
}>;
|
|
146
|
-
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
147
|
-
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
148
|
-
status: import("zod").ZodEnum<{
|
|
149
|
-
pending: "pending";
|
|
150
|
-
ready: "ready";
|
|
151
|
-
}>;
|
|
152
146
|
source: import("zod").ZodEnum<{
|
|
153
147
|
library: "library";
|
|
154
148
|
member: "member";
|
|
155
149
|
}>;
|
|
150
|
+
status: import("zod").ZodEnum<{
|
|
151
|
+
pending: "pending";
|
|
152
|
+
ready: "ready";
|
|
153
|
+
}>;
|
|
156
154
|
uploadedBy: import("zod").ZodString;
|
|
157
155
|
filename: import("zod").ZodString;
|
|
158
156
|
contentType: import("zod").ZodString;
|
|
159
157
|
size: import("zod").ZodNumber;
|
|
158
|
+
createdAt: import("zod").ZodCoercedDate<unknown>;
|
|
159
|
+
updatedAt: import("zod").ZodCoercedDate<unknown>;
|
|
160
160
|
}, import("zod/v4/core").$strip>>;
|
|
161
161
|
};
|
|
162
162
|
type AssetCreated = EventOf<typeof assetEvents.assetCreated>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SpaceImage } from "../types/schemas/content.js";
|
|
2
|
+
import "../types/index.js";
|
|
1
3
|
//#region src/assets/file-urls.d.ts
|
|
2
4
|
type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';
|
|
3
5
|
/** URLs for every file a response refers to. A public image (a profile photo, or the
|
|
@@ -6,6 +8,7 @@ type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';
|
|
|
6
8
|
interface FileUrls {
|
|
7
9
|
publicImage(orgId: string, ownerId: string, assetId: string): string;
|
|
8
10
|
thumbnail(owner: ThumbnailOwner, ownerId: string): string;
|
|
11
|
+
spaceImage(spaceId: string, image: SpaceImage): string;
|
|
9
12
|
activityAsset(activityId: string, assetId: string): string;
|
|
10
13
|
downloadFile(downloadId: string, assetId: string): string;
|
|
11
14
|
eventFile(eventId: string, assetId: string): string;
|
package/dist/assets/file-urls.js
CHANGED
|
@@ -18,6 +18,7 @@ function createFileUrls(apiUrl, publicUrl) {
|
|
|
18
18
|
return {
|
|
19
19
|
publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),
|
|
20
20
|
thumbnail: (owner, ownerId) => at("member", THUMBNAIL_PATHS[owner], ownerId, "thumbnail"),
|
|
21
|
+
spaceImage: (spaceId, image) => at("member", "spaces", spaceId, "images", image),
|
|
21
22
|
activityAsset: (activityId, assetId) => at("member", "activities", activityId, "assets", assetId),
|
|
22
23
|
downloadFile: (downloadId, assetId) => at("member", "downloads", downloadId, "assets", assetId),
|
|
23
24
|
eventFile: (eventId, assetId) => at("member", "events", eventId, "assets", assetId),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX } from '../types/index.js';\n\nexport type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';\n\n/** URLs for every file a response refers to. A public image (a profile photo, or the\n * thumbnail or cover of a published item) is a public storage URL; every other file is a\n * stable, access-checked URL on the API that redirects to a signed storage URL. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n thumbnail(owner: ThumbnailOwner, ownerId: string): string;\n activityAsset(activityId: string, assetId: string): string;\n downloadFile(downloadId: string, assetId: string): string;\n eventFile(eventId: string, assetId: string): string;\n postCover(postId: string): string;\n postAsset(postId: string, assetId: string): string;\n messageAttachment(chatId: string, messageId: string, assetId: string): string;\n memberUpload(assetId: string): string;\n libraryFile(assetId: string): string;\n libraryDownload(assetId: string): string;\n}\n\nconst THUMBNAIL_PATHS: Record<ThumbnailOwner, string> = {\n course: 'courses',\n download: 'downloads',\n community: 'communities',\n event: 'events',\n};\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls {\n const base = apiUrl.replace(/\\/+$/, '');\n const at = (...segments: string[]) =>\n `${base}/api/${segments.map((s) => encodeURIComponent(s)).join('/')}`;\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n thumbnail: (owner, ownerId) => at('member', THUMBNAIL_PATHS[owner], ownerId, 'thumbnail'),\n activityAsset: (activityId, assetId) =>\n at('member', 'activities', activityId, 'assets', assetId),\n downloadFile: (downloadId, assetId) => at('member', 'downloads', downloadId, 'assets', assetId),\n eventFile: (eventId, assetId) => at('member', 'events', eventId, 'assets', assetId),\n postCover: (postId) => at('member', 'posts', postId, 'cover'),\n postAsset: (postId, assetId) => at('member', 'posts', postId, 'assets', assetId),\n messageAttachment: (chatId, messageId, assetId) =>\n at('member', 'chats', chatId, 'messages', messageId, 'attachments', assetId),\n memberUpload: (assetId) => at('member', 'uploads', assetId, 'file'),\n libraryFile: (assetId) => at('assets', assetId, 'file'),\n libraryDownload: (assetId) => at('assets', assetId, 'download'),\n };\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"file-urls.js","names":[],"sources":["../../src/assets/file-urls.ts"],"sourcesContent":["import { PUBLIC_PREFIX, type SpaceImage } from '../types/index.js';\n\nexport type ThumbnailOwner = 'course' | 'download' | 'community' | 'event';\n\n/** URLs for every file a response refers to. A public image (a profile photo, or the\n * thumbnail or cover of a published item) is a public storage URL; every other file is a\n * stable, access-checked URL on the API that redirects to a signed storage URL. */\nexport interface FileUrls {\n publicImage(orgId: string, ownerId: string, assetId: string): string;\n thumbnail(owner: ThumbnailOwner, ownerId: string): string;\n spaceImage(spaceId: string, image: SpaceImage): string;\n activityAsset(activityId: string, assetId: string): string;\n downloadFile(downloadId: string, assetId: string): string;\n eventFile(eventId: string, assetId: string): string;\n postCover(postId: string): string;\n postAsset(postId: string, assetId: string): string;\n messageAttachment(chatId: string, messageId: string, assetId: string): string;\n memberUpload(assetId: string): string;\n libraryFile(assetId: string): string;\n libraryDownload(assetId: string): string;\n}\n\nconst THUMBNAIL_PATHS: Record<ThumbnailOwner, string> = {\n course: 'courses',\n download: 'downloads',\n community: 'communities',\n event: 'events',\n};\n\nexport function publicImagePrefix(orgId: string, ownerId: string): string {\n return `${PUBLIC_PREFIX}${orgId}/${ownerId}/`;\n}\n\nexport function publicImageKey(orgId: string, ownerId: string, assetId: string): string {\n return `${publicImagePrefix(orgId, ownerId)}${assetId}`;\n}\n\nexport function createFileUrls(apiUrl: string, publicUrl: (key: string) => string): FileUrls {\n const base = apiUrl.replace(/\\/+$/, '');\n const at = (...segments: string[]) =>\n `${base}/api/${segments.map((s) => encodeURIComponent(s)).join('/')}`;\n return {\n publicImage: (orgId, ownerId, assetId) => publicUrl(publicImageKey(orgId, ownerId, assetId)),\n thumbnail: (owner, ownerId) => at('member', THUMBNAIL_PATHS[owner], ownerId, 'thumbnail'),\n spaceImage: (spaceId, image) => at('member', 'spaces', spaceId, 'images', image),\n activityAsset: (activityId, assetId) =>\n at('member', 'activities', activityId, 'assets', assetId),\n downloadFile: (downloadId, assetId) => at('member', 'downloads', downloadId, 'assets', assetId),\n eventFile: (eventId, assetId) => at('member', 'events', eventId, 'assets', assetId),\n postCover: (postId) => at('member', 'posts', postId, 'cover'),\n postAsset: (postId, assetId) => at('member', 'posts', postId, 'assets', assetId),\n messageAttachment: (chatId, messageId, assetId) =>\n at('member', 'chats', chatId, 'messages', messageId, 'attachments', assetId),\n memberUpload: (assetId) => at('member', 'uploads', assetId, 'file'),\n libraryFile: (assetId) => at('assets', assetId, 'file'),\n libraryDownload: (assetId) => at('assets', assetId, 'download'),\n };\n}\n"],"mappings":";;AAsBA,MAAM,kBAAkD;CACtD,QAAQ;CACR,UAAU;CACV,WAAW;CACX,OAAO;AACT;AAEA,SAAgB,kBAAkB,OAAe,SAAyB;CACxE,OAAO,GAAG,gBAAgB,MAAM,GAAG,QAAQ;AAC7C;AAEA,SAAgB,eAAe,OAAe,SAAiB,SAAyB;CACtF,OAAO,GAAG,kBAAkB,OAAO,OAAO,IAAI;AAChD;AAEA,SAAgB,eAAe,QAAgB,WAA8C;CAC3F,MAAM,OAAO,OAAO,QAAQ,QAAQ,EAAE;CACtC,MAAM,MAAM,GAAG,aACb,GAAG,KAAK,OAAO,SAAS,KAAK,MAAM,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG;CACpE,OAAO;EACL,cAAc,OAAO,SAAS,YAAY,UAAU,eAAe,OAAO,SAAS,OAAO,CAAC;EAC3F,YAAY,OAAO,YAAY,GAAG,UAAU,gBAAgB,QAAQ,SAAS,WAAW;EACxF,aAAa,SAAS,UAAU,GAAG,UAAU,UAAU,SAAS,UAAU,KAAK;EAC/E,gBAAgB,YAAY,YAC1B,GAAG,UAAU,cAAc,YAAY,UAAU,OAAO;EAC1D,eAAe,YAAY,YAAY,GAAG,UAAU,aAAa,YAAY,UAAU,OAAO;EAC9F,YAAY,SAAS,YAAY,GAAG,UAAU,UAAU,SAAS,UAAU,OAAO;EAClF,YAAY,WAAW,GAAG,UAAU,SAAS,QAAQ,OAAO;EAC5D,YAAY,QAAQ,YAAY,GAAG,UAAU,SAAS,QAAQ,UAAU,OAAO;EAC/E,oBAAoB,QAAQ,WAAW,YACrC,GAAG,UAAU,SAAS,QAAQ,YAAY,WAAW,eAAe,OAAO;EAC7E,eAAe,YAAY,GAAG,UAAU,WAAW,SAAS,MAAM;EAClE,cAAc,YAAY,GAAG,UAAU,SAAS,MAAM;EACtD,kBAAkB,YAAY,GAAG,UAAU,SAAS,UAAU;CAChE;AACF"}
|
package/dist/assets/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
2
1
|
import { assetEvents } from "./events.js";
|
|
2
|
+
import { createFileUrls, publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
3
3
|
import { AssetsServiceImpl, MEMBER_IMAGE_TYPE, isMemberImageType, kindOf, memberKindOf } from "./service.js";
|
|
4
4
|
import { UPLOADS_REMOVE_UNATTACHED_JOB, assetsJobs, assetsRecurringJobs } from "./jobs.js";
|
|
5
5
|
export { AssetsServiceImpl, MEMBER_IMAGE_TYPE, UPLOADS_REMOVE_UNATTACHED_JOB, assetEvents, assetsJobs, assetsRecurringJobs, createFileUrls, isMemberImageType, kindOf, memberKindOf, publicImageKey, publicImagePrefix };
|
package/dist/assets/ports.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ interface AssetFiles {
|
|
|
39
39
|
signFile(orgId: string, assetId: string, opts?: {
|
|
40
40
|
saveAs?: string;
|
|
41
41
|
}): Promise<string | null>;
|
|
42
|
-
|
|
42
|
+
syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
|
|
43
43
|
}
|
|
44
44
|
interface MemberUploadInput {
|
|
45
45
|
filename: string;
|
package/dist/assets/service.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ declare class AssetsServiceImpl implements AssetsService {
|
|
|
50
50
|
signFile(orgId: string, assetId: string, opts?: {
|
|
51
51
|
saveAs?: string;
|
|
52
52
|
}): Promise<string | null>;
|
|
53
|
-
|
|
53
|
+
syncPublicImages(orgId: string, ownerId: string, assetIds: readonly string[]): Promise<void>;
|
|
54
54
|
libraryFile(assetId: string, opts: {
|
|
55
55
|
download: boolean;
|
|
56
56
|
}, actor: Actor): Promise<string>;
|
package/dist/assets/service.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
3
|
-
import { genId } from "../shared/id.js";
|
|
1
|
+
import { assetEvents } from "./events.js";
|
|
4
2
|
import { PUBLIC_PREFIX } from "../types/ports.js";
|
|
5
3
|
import { publicImageKey, publicImagePrefix } from "./file-urls.js";
|
|
6
|
-
import {
|
|
4
|
+
import { genId } from "../shared/id.js";
|
|
5
|
+
import { ConflictError, ForbiddenError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
6
|
+
import { noopLogger } from "../shared/logger.js";
|
|
7
7
|
//#region src/assets/service.ts
|
|
8
8
|
const WRITE_PERMISSION = {
|
|
9
9
|
course: "courses:write",
|
|
@@ -264,35 +264,40 @@ var AssetsServiceImpl = class {
|
|
|
264
264
|
expiresInSeconds: this.downloadLinkExpirySeconds
|
|
265
265
|
});
|
|
266
266
|
}
|
|
267
|
-
async
|
|
267
|
+
async syncPublicImages(orgId, ownerId, assetIds) {
|
|
268
268
|
try {
|
|
269
|
-
const wanted =
|
|
269
|
+
const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));
|
|
270
270
|
const held = await this.storage.list(publicImagePrefix(orgId, ownerId));
|
|
271
|
-
for (const object of held) if (object.key
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
-
|
|
271
|
+
for (const object of held) if (!wanted.has(object.key)) await this.storage.remove(object.key);
|
|
272
|
+
const heldKeys = new Set(held.map((object) => object.key));
|
|
273
|
+
const missing = [...wanted].filter(([key]) => !heldKeys.has(key));
|
|
274
|
+
if (missing.length === 0) return;
|
|
275
|
+
const ready = new Map((await this.readyAmong(orgId, missing.map(([, id]) => id))).map((asset) => [asset.id, asset]));
|
|
276
|
+
for (const [key, assetId] of missing) {
|
|
277
|
+
const asset = ready.get(assetId);
|
|
278
|
+
if (!asset || asset.kind !== "image") {
|
|
279
|
+
this.logger.warn("public image not published — no ready image", {
|
|
280
|
+
orgId,
|
|
281
|
+
ownerId,
|
|
282
|
+
assetId
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
await this.storage.publish({
|
|
287
|
+
from: asset.key,
|
|
288
|
+
to: key
|
|
289
|
+
});
|
|
290
|
+
this.logger.info("public image published", {
|
|
276
291
|
orgId,
|
|
277
292
|
ownerId,
|
|
278
293
|
assetId
|
|
279
294
|
});
|
|
280
|
-
return;
|
|
281
295
|
}
|
|
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
296
|
} catch (err) {
|
|
292
|
-
this.logger.error("public
|
|
297
|
+
this.logger.error("public images not synced", {
|
|
293
298
|
orgId,
|
|
294
299
|
ownerId,
|
|
295
|
-
|
|
300
|
+
assetIds,
|
|
296
301
|
err
|
|
297
302
|
});
|
|
298
303
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.js';\nimport type {\n Asset,\n AssetKind,\n AssetView,\n AssetsQuery,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type {\n AssetsRepository,\n AssetsService,\n AssetsUnitOfWork,\n MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix, type FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\n}\n\nexport function kindOf(contentType: string): AssetKind {\n const type = contentType.trim().toLowerCase();\n if (type.startsWith('image/')) {\n return 'image';\n }\n if (type.startsWith('video/')) {\n return 'video';\n }\n if (type.startsWith('audio/')) {\n return 'audio';\n }\n return 'document';\n}\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nfunction storedKey(orgId: string, folder: string, id: string, filename: string): string {\n return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;\n}\n\nfunction finalMemberKey(orgId: string, asset: Asset): string {\n return `${orgPrefix(orgId)}${asset.kind}/${asset.id}/${sanitizeFilename(asset.filename)}`;\n}\n\nfunction graceCutoff(now: Date): Date {\n return new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n memberImageMaxBytes: number;\n memberFileMaxBytes: number;\n downloadLinkExpirySeconds: number;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly memberImageMaxBytes: number;\n private readonly memberFileMaxBytes: number;\n private readonly downloadLinkExpirySeconds: number;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) {\n throw new Error(\n `invalid memberImageMaxBytes \"${params.memberImageMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (\n !Number.isSafeInteger(params.downloadLinkExpirySeconds) ||\n params.downloadLinkExpirySeconds <= 0\n ) {\n throw new Error(\n `invalid downloadLinkExpirySeconds \"${params.downloadLinkExpirySeconds}\" (expected a positive integer)`,\n );\n }\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.memberImageMaxBytes = params.memberImageMaxBytes;\n this.memberFileMaxBytes = params.memberFileMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : kindOf(input.contentType);\n const folder = input.source === 'member' ? 'uploads' : kind;\n const key = storedKey(orgId, folder, id, input.filename);\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind,\n source: input.source,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n contentId: input.contentId,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', {\n orgId,\n assetId: id,\n kind,\n source: input.source,\n contentId: input.contentId,\n });\n return {\n asset: { ...asset, url: null, downloadUrl: null },\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n if (asset.source === 'member') {\n throw new ForbiddenError('Only the uploader can finalize a member upload');\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return confirmed.status === 'ready'\n ? { ...confirmed, url: this.fileUrls.memberUpload(confirmed.id), downloadUrl: null }\n : { ...confirmed, url: null, downloadUrl: null };\n }\n\n async ownUploadFile(assetId: string, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n if (!(await this.ownsUpload(orgId, assetId, actor.id))) {\n this.logger.warn('upload file not served — not the uploader', {\n orgId,\n assetId,\n orgUserId: actor.id,\n });\n throw new NotFoundError('Upload', assetId);\n }\n const url = await this.signFile(orgId, assetId);\n if (!url) {\n throw new NotFoundError('Upload', assetId);\n }\n return url;\n }\n\n async ownsUpload(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\n }\n\n async ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.kind === 'image' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\n const page = await this.repo.list(orgId, { ...query, source: query.source ?? 'library' });\n return { ...page, rows: await Promise.all(page.rows.map((asset) => this.view(asset))) };\n }\n\n async get(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\n }\n\n async readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>> {\n const ready = await this.readyAmong(orgId, assetIds);\n return Object.fromEntries(\n ready.map((asset) => [\n asset.id,\n { filename: asset.filename, contentType: asset.contentType, size: asset.size },\n ]),\n );\n }\n\n async signFile(\n orgId: string,\n assetId: string,\n opts: { saveAs?: string } = {},\n ): Promise<string | null> {\n const [asset] = await this.readyAmong(orgId, [assetId]);\n if (!asset) {\n this.logger.warn('file not served — no ready asset', { orgId, assetId });\n return null;\n }\n return opts.saveAs === undefined\n ? this.storage.presignDownload({ key: asset.key })\n : this.storage.presignDownload({\n key: asset.key,\n downloadFilename: opts.saveAs,\n expiresInSeconds: this.downloadLinkExpirySeconds,\n });\n }\n\n async 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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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":[],"sources":["../../src/assets/service.ts"],"sourcesContent":["import { genId } from '../shared/id.js';\nimport type { Logger, ObjectStorage } from '../shared/ports.js';\nimport { PUBLIC_PREFIX } from '../types/index.js';\nimport {\n ConflictError,\n ForbiddenError,\n InvalidInputError,\n NotFoundError,\n} from '../shared/errors.js';\nimport type { Actor } from '../shared/actor.js';\nimport type { AssetSource, ContentType, Permission } from '../types/index.js';\nimport type {\n Asset,\n AssetKind,\n AssetView,\n AssetsQuery,\n Page,\n RequestUploadInput,\n UploadTicket,\n} from './model.js';\nimport type {\n AssetsRepository,\n AssetsService,\n AssetsUnitOfWork,\n MemberUpload,\n MemberUploadInput,\n ReadyFile,\n} from './ports.js';\nimport { publicImageKey, publicImagePrefix, type FileUrls } from './file-urls.js';\nimport { noopLogger } from '../shared/logger.js';\nimport { assetEvents } from './events.js';\n\nconst WRITE_PERMISSION: Record<ContentType, Permission> = {\n course: 'courses:write',\n download: 'downloads:write',\n community: 'communities:write',\n event: 'events:write',\n};\n\nconst UNATTACHED_UPLOAD_GRACE_MS = 60 * 60 * 1000;\n\nexport const MEMBER_IMAGE_TYPE = /^image\\/(png|jpeg|gif|webp|avif)$/i;\n\nexport function isMemberImageType(contentType: string): boolean {\n return MEMBER_IMAGE_TYPE.test(contentType.trim());\n}\n\nexport function memberKindOf(contentType: string): AssetKind {\n if (isMemberImageType(contentType)) {\n return 'image';\n }\n const kind = kindOf(contentType);\n return kind === 'image' ? 'document' : kind;\n}\n\nexport function kindOf(contentType: string): AssetKind {\n const type = contentType.trim().toLowerCase();\n if (type.startsWith('image/')) {\n return 'image';\n }\n if (type.startsWith('video/')) {\n return 'video';\n }\n if (type.startsWith('audio/')) {\n return 'audio';\n }\n return 'document';\n}\n\nfunction orgPrefix(orgId: string): string {\n return `org/${orgId}/`;\n}\n\nfunction sanitizeFilename(filename: string): string {\n const base = filename.split(/[\\\\/]/).pop() ?? 'file';\n return base.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) || 'file';\n}\n\nfunction storedKey(orgId: string, folder: string, id: string, filename: string): string {\n return `${orgPrefix(orgId)}${folder}/${id}/${sanitizeFilename(filename)}`;\n}\n\nfunction finalMemberKey(orgId: string, asset: Asset): string {\n return `${orgPrefix(orgId)}${asset.kind}/${asset.id}/${sanitizeFilename(asset.filename)}`;\n}\n\nfunction graceCutoff(now: Date): Date {\n return new Date(now.getTime() - UNATTACHED_UPLOAD_GRACE_MS);\n}\n\nexport type AssetsServiceParams = {\n storage: ObjectStorage;\n repo: AssetsRepository;\n uow: AssetsUnitOfWork;\n memberImageMaxBytes: number;\n memberFileMaxBytes: number;\n downloadLinkExpirySeconds: number;\n fileUrls: FileUrls;\n logger?: Logger;\n};\n\nexport class AssetsServiceImpl implements AssetsService {\n private readonly storage: ObjectStorage;\n private readonly repo: AssetsRepository;\n private readonly uow: AssetsUnitOfWork;\n private readonly memberImageMaxBytes: number;\n private readonly memberFileMaxBytes: number;\n private readonly downloadLinkExpirySeconds: number;\n private readonly fileUrls: FileUrls;\n private readonly logger: Logger;\n\n constructor(params: AssetsServiceParams) {\n if (!Number.isSafeInteger(params.memberImageMaxBytes) || params.memberImageMaxBytes <= 0) {\n throw new Error(\n `invalid memberImageMaxBytes \"${params.memberImageMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (!Number.isSafeInteger(params.memberFileMaxBytes) || params.memberFileMaxBytes <= 0) {\n throw new Error(\n `invalid memberFileMaxBytes \"${params.memberFileMaxBytes}\" (expected a positive integer)`,\n );\n }\n if (\n !Number.isSafeInteger(params.downloadLinkExpirySeconds) ||\n params.downloadLinkExpirySeconds <= 0\n ) {\n throw new Error(\n `invalid downloadLinkExpirySeconds \"${params.downloadLinkExpirySeconds}\" (expected a positive integer)`,\n );\n }\n this.storage = params.storage;\n this.repo = params.repo;\n this.uow = params.uow;\n this.memberImageMaxBytes = params.memberImageMaxBytes;\n this.memberFileMaxBytes = params.memberFileMaxBytes;\n this.downloadLinkExpirySeconds = params.downloadLinkExpirySeconds;\n this.fileUrls = params.fileUrls;\n this.logger = params.logger ?? noopLogger;\n }\n\n async requestUpload(input: RequestUploadInput, actor: Actor): Promise<UploadTicket> {\n const orgId = actor.orgId;\n if (input.contentId === undefined) {\n actor.permissions.require('media:write');\n } else {\n const type = await this.repo.contentTypeOf(orgId, input.contentId);\n if (!type) {\n throw new NotFoundError('Content item', input.contentId);\n }\n if (\n !actor.permissions.has('media:write') &&\n !actor.permissions.has(WRITE_PERMISSION[type], input.contentId)\n ) {\n this.logger.warn('asset upload refused', {\n orgId,\n orgUserId: actor.id,\n contentId: input.contentId,\n });\n throw new ForbiddenError(`You need media:write or ${WRITE_PERMISSION[type]}`);\n }\n }\n return this.createUpload(orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'library',\n uploadedBy: actor.id,\n contentId: input.contentId ?? null,\n });\n }\n\n async requestMemberUpload(input: MemberUploadInput, actor: Actor): Promise<UploadTicket> {\n return this.createUpload(actor.orgId, {\n filename: input.filename,\n contentType: input.contentType,\n source: 'member',\n uploadedBy: actor.id,\n contentId: null,\n });\n }\n\n private async createUpload(\n orgId: string,\n input: {\n filename: string;\n contentType: string;\n source: AssetSource;\n uploadedBy: string;\n contentId: string | null;\n },\n ): Promise<UploadTicket> {\n const id = genId('asset');\n const kind =\n input.source === 'member' ? memberKindOf(input.contentType) : kindOf(input.contentType);\n const folder = input.source === 'member' ? 'uploads' : kind;\n const key = storedKey(orgId, folder, id, input.filename);\n const presigned = await this.storage.presignUpload({ key, contentType: input.contentType });\n const now = new Date();\n const asset = await this.uow.run(async ({ assets, outbox }) => {\n const asset = await assets.insert(orgId, {\n orgId,\n id,\n key,\n kind,\n source: input.source,\n filename: input.filename,\n contentType: input.contentType,\n size: 0,\n status: 'pending',\n uploadedBy: input.uploadedBy,\n contentId: input.contentId,\n createdAt: now,\n updatedAt: now,\n });\n await outbox.append([assetEvents.assetCreated.make({ orgId, data: asset })]);\n return asset;\n });\n this.logger.info('asset upload requested', {\n orgId,\n assetId: id,\n kind,\n source: input.source,\n contentId: input.contentId,\n });\n return {\n asset: { ...asset, url: null, downloadUrl: null },\n uploadUrl: presigned.url,\n method: 'PUT',\n expiresInSeconds: presigned.expiresInSeconds,\n headers: presigned.headers,\n };\n }\n\n async confirm(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n if (asset.source === 'member') {\n throw new ForbiddenError('Only the uploader can finalize a member upload');\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return this.view(confirmed);\n }\n\n async confirmOwn(id: string, actor: Actor): Promise<AssetView> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n this.logger.debug('upload confirm refused', { orgId, assetId: id, orgUserId: actor.id });\n throw new NotFoundError('Asset', id);\n }\n const confirmed = await this.finalize(orgId, asset);\n if (!confirmed) {\n throw new NotFoundError('Asset', id);\n }\n return confirmed.status === 'ready'\n ? { ...confirmed, url: this.fileUrls.memberUpload(confirmed.id), downloadUrl: null }\n : { ...confirmed, url: null, downloadUrl: null };\n }\n\n async ownUploadFile(assetId: string, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n if (!(await this.ownsUpload(orgId, assetId, actor.id))) {\n this.logger.warn('upload file not served — not the uploader', {\n orgId,\n assetId,\n orgUserId: actor.id,\n });\n throw new NotFoundError('Upload', assetId);\n }\n const url = await this.signFile(orgId, assetId);\n if (!url) {\n throw new NotFoundError('Upload', assetId);\n }\n return url;\n }\n\n async ownsUpload(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async memberUploads(orgId: string, assetIds: string[]): Promise<MemberUpload[]> {\n const ready = await this.readyAmong(orgId, assetIds);\n return ready\n .filter((asset) => asset.source === 'member')\n .map((asset) => ({\n id: asset.id,\n kind: asset.kind,\n filename: asset.filename,\n contentType: asset.contentType,\n size: asset.size,\n }));\n }\n\n async ownsImage(orgId: string, assetId: string, orgUserId: string): Promise<boolean> {\n const asset = await this.repo.findById(orgId, assetId);\n return (\n asset !== null &&\n asset.source === 'member' &&\n asset.kind === 'image' &&\n asset.status === 'ready' &&\n asset.uploadedBy === orgUserId\n );\n }\n\n async libraryAssetsFor(\n orgId: string,\n assetIds: string[],\n ): Promise<{ id: string; contentId: string | null }[]> {\n if (assetIds.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, [...new Set(assetIds)]);\n const library = new Map(\n rows\n .filter((a) => a.source === 'library' && a.status === 'ready')\n .map((a) => [a.id, a.contentId]),\n );\n return assetIds.flatMap((id) =>\n library.has(id) ? [{ id, contentId: library.get(id) ?? null }] : [],\n );\n }\n\n async list(query: AssetsQuery, actor: Actor): Promise<Page<AssetView>> {\n const orgId = actor.orgId;\n if (!actor.permissions.has('media:read')) {\n if (query.contentId === undefined || (query.source ?? 'library') !== 'library') {\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(orgId, query.contentId, actor);\n }\n const page = await this.repo.list(orgId, { ...query, source: query.source ?? 'library' });\n return { ...page, rows: await Promise.all(page.rows.map((asset) => this.view(asset))) };\n }\n\n async get(id: string, actor: Actor): Promise<AssetView> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset) {\n throw new NotFoundError('Asset', id);\n }\n await this.requireReach(asset, actor);\n return this.view(asset);\n }\n\n async readyFiles(orgId: string, assetIds: readonly string[]): Promise<Record<string, ReadyFile>> {\n const ready = await this.readyAmong(orgId, assetIds);\n return Object.fromEntries(\n ready.map((asset) => [\n asset.id,\n { filename: asset.filename, contentType: asset.contentType, size: asset.size },\n ]),\n );\n }\n\n async signFile(\n orgId: string,\n assetId: string,\n opts: { saveAs?: string } = {},\n ): Promise<string | null> {\n const [asset] = await this.readyAmong(orgId, [assetId]);\n if (!asset) {\n this.logger.warn('file not served — no ready asset', { orgId, assetId });\n return null;\n }\n return opts.saveAs === undefined\n ? this.storage.presignDownload({ key: asset.key })\n : this.storage.presignDownload({\n key: asset.key,\n downloadFilename: opts.saveAs,\n expiresInSeconds: this.downloadLinkExpirySeconds,\n });\n }\n\n async syncPublicImages(\n orgId: string,\n ownerId: string,\n assetIds: readonly string[],\n ): Promise<void> {\n try {\n const wanted = new Map(assetIds.map((id) => [publicImageKey(orgId, ownerId, id), id]));\n const held = await this.storage.list(publicImagePrefix(orgId, ownerId));\n for (const object of held) {\n if (!wanted.has(object.key)) {\n await this.storage.remove(object.key);\n }\n }\n const heldKeys = new Set(held.map((object) => object.key));\n const missing = [...wanted].filter(([key]) => !heldKeys.has(key));\n if (missing.length === 0) {\n return;\n }\n const ready = new Map(\n (\n await this.readyAmong(\n orgId,\n missing.map(([, id]) => id),\n )\n ).map((asset) => [asset.id, asset]),\n );\n for (const [key, assetId] of missing) {\n const asset = ready.get(assetId);\n if (!asset || asset.kind !== 'image') {\n this.logger.warn('public image not published — no ready image', {\n orgId,\n ownerId,\n assetId,\n });\n continue;\n }\n await this.storage.publish({ from: asset.key, to: key });\n this.logger.info('public image published', { orgId, ownerId, assetId });\n }\n } catch (err) {\n this.logger.error('public images not synced', { orgId, ownerId, assetIds, err });\n }\n }\n\n async libraryFile(assetId: string, opts: { download: boolean }, actor: Actor): Promise<string> {\n const orgId = actor.orgId;\n const asset = await this.repo.findById(orgId, assetId);\n if (!asset || asset.source !== 'library') {\n this.logger.warn('library file not served — not a library asset', { orgId, assetId });\n throw new NotFoundError('Asset', assetId);\n }\n await this.requireReach(asset, actor);\n const url = await this.signFile(\n orgId,\n assetId,\n opts.download ? { saveAs: asset.filename } : {},\n );\n if (!url) {\n throw new NotFoundError('Asset', assetId);\n }\n return url;\n }\n\n async remove(id: string, actor: Actor): Promise<void> {\n actor.permissions.require('media:delete');\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n async removeOwn(id: string, actor: Actor): Promise<void> {\n const asset = await this.repo.findById(actor.orgId, id);\n if (!asset || asset.source !== 'member' || asset.uploadedBy !== actor.id) {\n throw new NotFoundError('Asset', id);\n }\n await this.removeAsset(actor.orgId, id, actor.id);\n }\n\n private async removeAsset(orgId: string, id: string, actorId: string): Promise<void> {\n const asset = await this.repo.findById(orgId, id);\n if (!asset || !(await this.discard(orgId, asset))) {\n throw new NotFoundError('Asset', id);\n }\n this.logger.info('asset removed', { orgId, assetId: id, actorId });\n }\n\n private async requireReach(asset: Asset, actor: Actor): Promise<void> {\n if (actor.permissions.has('media:read')) {\n return;\n }\n if (asset.contentId === null) {\n this.logger.warn('asset refused', {\n orgId: actor.orgId,\n assetId: asset.id,\n orgUserId: actor.id,\n });\n throw new ForbiddenError('You need media:read');\n }\n await this.requireItemWrite(actor.orgId, asset.contentId, actor);\n }\n\n private async requireItemWrite(orgId: string, contentId: string, actor: Actor): Promise<void> {\n const type = await this.repo.contentTypeOf(orgId, contentId);\n if (!type || !actor.permissions.has(WRITE_PERMISSION[type], contentId)) {\n this.logger.warn('asset refused', { orgId, contentId, orgUserId: actor.id });\n throw new ForbiddenError(\n type ? `You need media:read or ${WRITE_PERMISSION[type]}` : 'You need media:read',\n );\n }\n }\n\n async removeUnattached(orgId: string, now: Date): Promise<number> {\n const stale = await this.repo.listUnreferencedMemberUploads(orgId, graceCutoff(now));\n let removed = 0;\n let failed = 0;\n for (const asset of stale) {\n try {\n if (await this.discard(orgId, asset)) {\n removed += 1;\n }\n } catch (err) {\n if (!(err instanceof ConflictError)) {\n failed += 1;\n this.logger.error('unattached upload not removed', { orgId, assetId: asset.id, err });\n }\n }\n }\n const orphans = await this.orphanPublicImages(orgId);\n for (const key of orphans) {\n await this.removeObject(orgId, key.split('/').pop() ?? '', key);\n }\n let stray = 0;\n for (const key of await this.strayUploads(orgId, now)) {\n try {\n await this.storage.remove(key);\n stray += 1;\n } catch (err) {\n failed += 1;\n this.logger.warn('stray upload not removed', { orgId, err });\n }\n }\n if (removed > 0 || stray > 0 || orphans.length > 0 || failed > 0) {\n this.logger.info('unattached uploads removed', {\n orgId,\n count: removed,\n stray,\n publicCopies: orphans.length,\n failed,\n });\n }\n return removed + stray + orphans.length;\n }\n\n private async orphanPublicImages(orgId: string): Promise<string[]> {\n const objects = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n if (objects.length === 0) {\n return [];\n }\n const assetIdOf = (key: string) => key.split('/').pop() ?? '';\n const live = new Set(\n (\n await this.readyAmong(\n orgId,\n objects.map((o) => assetIdOf(o.key)),\n )\n ).map((a) => a.id),\n );\n return objects.filter((o) => !live.has(assetIdOf(o.key))).map((o) => o.key);\n }\n\n private async strayUploads(orgId: string, now: Date): Promise<string[]> {\n const cutoff = now.getTime() - this.storage.uploadExpirySeconds * 1000;\n const objects = await this.storage.list(`${orgPrefix(orgId)}uploads/`);\n const old = objects\n .filter((o) => o.lastModified !== undefined && Date.parse(o.lastModified) < cutoff)\n .map((o) => o.key);\n if (old.length === 0) {\n return [];\n }\n const held = new Set(await this.repo.heldKeys(orgId, old));\n return old.filter((key) => !held.has(key));\n }\n\n private async finalize(orgId: string, asset: Asset): Promise<Asset | null> {\n if (asset.status !== 'pending') {\n return asset;\n }\n if (asset.source === 'member') {\n return this.finalizeMember(orgId, asset);\n }\n const stat = await this.storage.stat(asset.key);\n if (!stat) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const contentType = stat.contentType ?? asset.contentType;\n const ready = await this.markReady(orgId, asset, {\n key: asset.key,\n kind: kindOf(contentType),\n size: stat.size,\n contentType,\n });\n return ready ?? this.repo.findById(orgId, asset.id);\n }\n\n private async finalizeMember(orgId: string, asset: Asset): Promise<Asset | null> {\n if (!(await this.storage.stat(asset.key))) {\n this.logger.debug('asset not yet uploaded', { orgId, assetId: asset.id });\n return asset;\n }\n const key = finalMemberKey(orgId, asset);\n await this.storage.copy({ from: asset.key, to: key });\n const stat = await this.storage.stat(key);\n if (!stat) {\n throw new Error('copied upload not found');\n }\n const contentType = asset.contentType;\n const kind = memberKindOf(contentType);\n const limit = kind === 'image' ? this.memberImageMaxBytes : this.memberFileMaxBytes;\n const reason =\n stat.contentType !== undefined && stat.contentType !== contentType\n ? 'The uploaded file is not the declared type'\n : stat.size > limit\n ? `This ${kind === 'image' ? 'image' : 'file'} is over the ${sizeLabel(limit)} limit`\n : null;\n if (reason) {\n await this.removeObject(orgId, asset.id, key);\n await this.discard(orgId, asset);\n this.logger.info('member upload refused', { orgId, assetId: asset.id, reason });\n throw new InvalidInputError(reason);\n }\n const ready = await this.markReady(orgId, asset, { key, kind, size: stat.size, contentType });\n if (!ready) {\n return this.repo.findById(orgId, asset.id);\n }\n await this.removeObject(orgId, asset.id, asset.key);\n return ready;\n }\n\n private async markReady(\n orgId: string,\n asset: Asset,\n patch: Pick<Asset, 'key' | 'kind' | 'size' | 'contentType'>,\n ): Promise<Asset | null> {\n const updated = await this.uow.run(async ({ assets, outbox }) => {\n const updated = await assets.markReady(orgId, asset.id, patch);\n if (updated) {\n await outbox.append([assetEvents.assetReady.make({ orgId, data: updated })]);\n }\n return updated;\n });\n if (updated) {\n this.logger.info('asset confirmed', { orgId, assetId: asset.id });\n }\n return updated;\n }\n\n private async readyAmong(orgId: string, assetIds: readonly string[]): Promise<Asset[]> {\n const ids = [...new Set(assetIds)];\n if (ids.length === 0) {\n return [];\n }\n const rows = await this.repo.findByIds(orgId, ids);\n return rows.filter((asset) => asset.orgId === orgId && asset.status === 'ready');\n }\n\n private view(asset: Asset): AssetView {\n if (asset.source !== 'library' || asset.status !== 'ready') {\n return { ...asset, url: null, downloadUrl: null };\n }\n return {\n ...asset,\n url: this.fileUrls.libraryFile(asset.id),\n downloadUrl: this.fileUrls.libraryDownload(asset.id),\n };\n }\n\n private async discard(orgId: string, asset: Asset): Promise<boolean> {\n const deleted = await this.uow.run(async ({ assets, outbox }) => {\n const deleted = await assets.delete(orgId, asset.id);\n if (deleted) {\n await outbox.append([assetEvents.assetDeleted.make({ orgId, data: asset })]);\n }\n return deleted;\n });\n if (deleted) {\n await this.removeObject(orgId, asset.id, asset.key);\n await this.removePublicCopies(orgId, asset.id);\n }\n return deleted;\n }\n\n private async removePublicCopies(orgId: string, assetId: string): Promise<void> {\n try {\n const copies = await this.storage.list(`${PUBLIC_PREFIX}${orgId}/`);\n for (const copy of copies.filter((o) => o.key.endsWith(`/${assetId}`))) {\n await this.removeObject(orgId, assetId, copy.key);\n }\n } catch (err) {\n this.logger.warn('public copies not removed', { orgId, assetId, err });\n }\n }\n\n private async removeObject(orgId: string, assetId: string, key: string): Promise<void> {\n try {\n await this.storage.remove(key);\n } catch (error) {\n this.logger.warn('stored object not removed', { orgId, assetId, err: error });\n }\n }\n}\n\nfunction sizeLabel(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB'];\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < units.length - 1) {\n value /= 1024;\n unit += 1;\n }\n return `${Number.isInteger(value) ? value : value.toFixed(1)} ${units[unit]}`;\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { entitlementEvents } from "../entitlements/events.js";
|
|
2
1
|
import { assetEvents } from "../assets/events.js";
|
|
2
|
+
import { entitlementEvents } from "../entitlements/events.js";
|
|
3
3
|
import { contentEvents } from "../content/events.js";
|
|
4
4
|
import { discussionEvents } from "../discussion/events.js";
|
|
5
5
|
import { identityEvents } from "../identity/events.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { noopLogger } from "../../shared/logger.js";
|
|
2
|
-
import { NotFoundError } from "../../shared/errors.js";
|
|
3
1
|
import { genId } from "../../shared/id.js";
|
|
2
|
+
import { NotFoundError } from "../../shared/errors.js";
|
|
3
|
+
import { noopLogger } from "../../shared/logger.js";
|
|
4
4
|
import { resolveChannels } from "../../shared/notification-settings.js";
|
|
5
5
|
//#region src/automations/notifications/processor.ts
|
|
6
6
|
const IN_APP = "inApp";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deliverWebhookInputSchema } from "../types/schemas/automations.js";
|
|
2
|
-
import { noopLogger } from "../shared/logger.js";
|
|
3
2
|
import { ConflictError, InvalidInputError, NotFoundError } from "../shared/errors.js";
|
|
3
|
+
import { noopLogger } from "../shared/logger.js";
|
|
4
4
|
import { ActionFailedError, AutomationKindMismatchError, InvalidTriggerError, ReservedActionError } from "./model.js";
|
|
5
5
|
import { DELIVER_WEBHOOK_ACTION, catalogActions, catalogTriggers, publicDataSchema } from "./catalog.js";
|
|
6
6
|
import { generateWebhookSecret } from "./webhook-signing.js";
|