@rpcbase/server 0.548.0 → 0.549.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.
@@ -6,7 +6,9 @@ export type UploadFileProcessorContext = {
6
6
  };
7
7
  export type UploadFileProcessorResult = {
8
8
  data: Buffer;
9
+ filename?: string;
9
10
  mimeType?: string;
11
+ metadata?: Record<string, unknown>;
10
12
  };
11
13
  export type UploadFileProcessor = {
12
14
  id: string;
@@ -19,7 +21,9 @@ export declare const getMaxUploadProcessorBytes: () => number;
19
21
  export declare const selectUploadProcessors: (ctx: UploadFileProcessorContext) => UploadFileProcessor[];
20
22
  export declare const applyUploadProcessors: (data: Buffer, ctx: Omit<UploadFileProcessorContext, "sniff" | "totalSize">) => Promise<{
21
23
  data: Buffer;
24
+ filename: string;
22
25
  mimeType: string;
23
26
  applied: string[];
27
+ metadata: Record<string, unknown>;
24
28
  }>;
25
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/processors/index.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,CAAC,GAAG,EAAE,0BAA0B,KAAK,OAAO,CAAA;IACnD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,0BAA0B,KAAK,OAAO,CAAC,yBAAyB,CAAC,GAAG,yBAAyB,CAAA;CAC3H,CAAA;AAED,eAAO,MAAM,gBAAgB,gCAAwE,CAAA;AAErG,eAAO,MAAM,0BAA0B,QAAO,MACqC,CAAA;AAEnF,eAAO,MAAM,sBAAsB,GAAI,KAAK,0BAA0B,KAAG,mBAAmB,EAC9B,CAAA;AAE9D,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,KAAK,IAAI,CAAC,0BAA0B,EAAE,OAAO,GAAG,WAAW,CAAC,KAC3D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAgC/D,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/processors/index.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,0BAA0B,GAAG;IACvC,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,CAAC,GAAG,EAAE,0BAA0B,KAAK,OAAO,CAAA;IACnD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,0BAA0B,KAAK,OAAO,CAAC,yBAAyB,CAAC,GAAG,yBAAyB,CAAA;CAC3H,CAAA;AAED,eAAO,MAAM,gBAAgB,gCAAoG,CAAA;AAEjI,eAAO,MAAM,0BAA0B,QAAO,MACqC,CAAA;AAEnF,eAAO,MAAM,sBAAsB,GAAI,KAAK,0BAA0B,KAAG,mBAAmB,EAC9B,CAAA;AAE9D,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,KAAK,IAAI,CAAC,0BAA0B,EAAE,OAAO,GAAG,WAAW,CAAC,KAC3D,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA0CpH,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"deleteFile.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/files/handlers/deleteFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIzC,OAAO,EAA2D,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAGrH,KAAK,qBAAqB,GAAG;IAC3B,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAQD,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,qBAAqB,EAAE,WAAW,CAiE5F,CAAA"}
1
+ {"version":3,"file":"deleteFile.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/files/handlers/deleteFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIzC,OAAO,EAA2D,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAGrH,KAAK,qBAAqB,GAAG;IAC3B,EAAE,EAAE,OAAO,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAqBD,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,qBAAqB,EAAE,WAAW,CAsE5F,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/files/handlers/getFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIzC,OAAO,EAA2D,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAqBrH,eAAO,MAAM,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,CA0GzF,CAAA"}
1
+ {"version":3,"file":"getFile.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/files/handlers/getFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIzC,OAAO,EAA2D,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAmFrH,eAAO,MAAM,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,CAkHzF,CAAA"}
@@ -0,0 +1,573 @@
1
+ import { queue } from "@rpcbase/worker";
2
+ import sharp from "sharp";
3
+ import { JSDOM } from "jsdom";
4
+ import createDOMPurify from "dompurify";
5
+ import { getTenantFilesystemDb } from "@rpcbase/db";
6
+ import { GridFSBucket, ObjectId } from "mongodb";
7
+ import { createHash } from "node:crypto";
8
+ import { buildAbilityFromSession, getAccessibleByQuery } from "@rpcbase/db/acl";
9
+ const MAX_HEIF_BYTES = 25 * 1024 * 1024;
10
+ const MAX_HEIF_INPUT_PIXELS = 64 * 1024 * 1024;
11
+ const WEBP_QUALITY = 82;
12
+ const CONVERSION_TIMEOUT_MS = 6e4;
13
+ const POLL_INTERVAL_MS = 250;
14
+ const HEIF_DECODE_UNSUPPORTED = "heif_decode_unsupported";
15
+ const heifMimeTypes = /* @__PURE__ */ new Set(["image/heic", "image/heif", "image/heic-sequence", "image/heif-sequence"]);
16
+ const heifExtensions = /\.(?:heic|heif|heics|heifs)$/i;
17
+ const hevcBrands = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "hevm", "hevs"]);
18
+ const convertHeifToWebpTaskName = "rb-upload-convert-heif-to-webp";
19
+ const delay = async (ms) => {
20
+ await new Promise((resolve) => setTimeout(resolve, ms));
21
+ };
22
+ const normalizeMimeType$1 = (value) => value.trim().toLowerCase();
23
+ const getFtypBrands = (sniff) => {
24
+ if (sniff.length < 12) return [];
25
+ const ftypOffset = sniff.indexOf(Buffer.from("ftyp"));
26
+ if (ftypOffset < 4 || ftypOffset > 32) return [];
27
+ const brands = [];
28
+ for (let offset = ftypOffset + 4; offset + 4 <= Math.min(sniff.length, ftypOffset + 80); offset += 4) {
29
+ const brand = sniff.subarray(offset, offset + 4).toString("ascii");
30
+ if (/^[a-zA-Z0-9 ]{4}$/.test(brand)) {
31
+ brands.push(brand.trim());
32
+ }
33
+ }
34
+ return brands.filter(Boolean);
35
+ };
36
+ const hasHeifDeclaration = ({
37
+ filename,
38
+ clientMimeType
39
+ }) => heifMimeTypes.has(normalizeMimeType$1(clientMimeType)) || heifExtensions.test(filename);
40
+ const looksLikeHevcHeif = (sniff) => getFtypBrands(sniff).some((brand) => hevcBrands.has(brand));
41
+ const isHeifUpload = ({
42
+ filename,
43
+ clientMimeType,
44
+ sniff
45
+ }) => hasHeifDeclaration({
46
+ filename,
47
+ clientMimeType
48
+ }) || looksLikeHevcHeif(sniff);
49
+ const toWebpFilename = (filename) => {
50
+ const trimmed = filename.trim();
51
+ if (!trimmed) return "image.webp";
52
+ if (heifExtensions.test(trimmed)) return trimmed.replace(heifExtensions, ".webp");
53
+ return `${trimmed}.webp`;
54
+ };
55
+ const isHeifDecodeUnsupportedError = (error) => {
56
+ const message = error instanceof Error ? error.message : String(error ?? "unknown");
57
+ const normalized = message.toLowerCase();
58
+ return message.startsWith(HEIF_DECODE_UNSUPPORTED) || normalized.includes("support for this compression format has not been built in") || normalized.includes("heif: error while loading plugin") || normalized.includes("unsupported feature: unsupported codec") || normalized.includes("no decoding plugin installed");
59
+ };
60
+ const getSharpHeifSupportDiagnostic = () => {
61
+ const suffixes = sharp.format.heif.input.fileSuffix ?? [];
62
+ const advertisedSuffixes = suffixes.length ? suffixes.join(", ") : "none";
63
+ const sharpVersion = sharp.versions.sharp ?? "unknown";
64
+ const vipsVersion = sharp.versions.vips ?? "unknown";
65
+ const heifVersion = sharp.versions.heif ?? "not reported";
66
+ return `Sharp ${sharpVersion} / libvips ${vipsVersion} / libheif ${heifVersion} advertises HEIF input suffixes: ${advertisedSuffixes}. iPhone HEIC/HEVC requires a libvips build with libheif, libde265 and x265/HEVC support. Install a compatible global libvips and reinstall sharp so it links against it.`;
67
+ };
68
+ const createHeifDecodeUnsupportedError = () => new Error(`${HEIF_DECODE_UNSUPPORTED}: ${getSharpHeifSupportDiagnostic()}`);
69
+ const normalizeConversionError = (error) => {
70
+ const message = error instanceof Error ? error.message : String(error ?? "unknown");
71
+ if (message === "heif_too_large") {
72
+ return new Error(message);
73
+ }
74
+ if (message.startsWith(HEIF_DECODE_UNSUPPORTED)) {
75
+ return new Error(message);
76
+ }
77
+ if (isHeifDecodeUnsupportedError(error)) {
78
+ return createHeifDecodeUnsupportedError();
79
+ }
80
+ return new Error("heif_conversion_failed");
81
+ };
82
+ const convertHeifToWebp = async (input, payload) => {
83
+ try {
84
+ const output = await sharp(input, {
85
+ limitInputPixels: MAX_HEIF_INPUT_PIXELS
86
+ }).rotate().webp({
87
+ quality: WEBP_QUALITY,
88
+ effort: 4
89
+ }).toBuffer({
90
+ resolveWithObject: true
91
+ });
92
+ return {
93
+ dataBase64: output.data.toString("base64"),
94
+ filename: toWebpFilename(payload.filename),
95
+ mimeType: "image/webp",
96
+ metadata: {
97
+ sourceFilename: payload.filename,
98
+ sourceMimeType: payload.mimeType,
99
+ ...typeof output.info.width === "number" ? {
100
+ width: output.info.width
101
+ } : {},
102
+ ...typeof output.info.height === "number" ? {
103
+ height: output.info.height
104
+ } : {}
105
+ }
106
+ };
107
+ } catch (error) {
108
+ throw normalizeConversionError(error);
109
+ }
110
+ };
111
+ const convertHeifToWebpTask = async (payload, job) => {
112
+ const input = Buffer.from(payload.inputBase64, "base64");
113
+ await job.log(`convert ${payload.filename} (${input.length} bytes) to webp`);
114
+ return convertHeifToWebp(input, payload);
115
+ };
116
+ queue.registerTask(convertHeifToWebpTaskName, convertHeifToWebpTask);
117
+ const waitForConversionResult = async (job) => {
118
+ const startedAt = Date.now();
119
+ while (Date.now() - startedAt < CONVERSION_TIMEOUT_MS) {
120
+ const currentJob = job.id ? await queue.getJob(String(job.id)) : job;
121
+ if (!currentJob) {
122
+ throw new Error("heif_conversion_failed");
123
+ }
124
+ const state = await currentJob.getState();
125
+ if (state === "completed") {
126
+ return currentJob.returnvalue;
127
+ }
128
+ if (state === "failed") {
129
+ throw new Error(currentJob.failedReason || "heif_conversion_failed");
130
+ }
131
+ await delay(POLL_INTERVAL_MS);
132
+ }
133
+ throw new Error("heif_conversion_timeout");
134
+ };
135
+ const runConversionTask = async (input, payload) => {
136
+ const job = await queue.add(convertHeifToWebpTaskName, {
137
+ ...payload,
138
+ inputBase64: input.toString("base64")
139
+ }, {
140
+ attempts: 1,
141
+ removeOnComplete: 128,
142
+ removeOnFail: 128
143
+ });
144
+ return waitForConversionResult(job);
145
+ };
146
+ const convertHeifToWebpProcessor = {
147
+ id: "convert-heif-to-webp",
148
+ maxBytes: MAX_HEIF_BYTES,
149
+ match: isHeifUpload,
150
+ process: async (data, ctx) => {
151
+ if (data.length > MAX_HEIF_BYTES) {
152
+ throw new Error("heif_too_large");
153
+ }
154
+ const result = await runConversionTask(data, {
155
+ filename: ctx.filename,
156
+ mimeType: ctx.clientMimeType
157
+ });
158
+ return {
159
+ data: Buffer.from(result.dataBase64, "base64"),
160
+ filename: result.filename,
161
+ mimeType: result.mimeType,
162
+ metadata: result.metadata
163
+ };
164
+ }
165
+ };
166
+ const MAX_SVG_BYTES = 128 * 1024;
167
+ const window = new JSDOM("").window;
168
+ const DOMPurify = createDOMPurify(window);
169
+ const normalizeForSniff = (raw) => raw.replace(/^\uFEFF/, "").trimStart();
170
+ const looksLikeSvgText = (text) => {
171
+ const normalized = normalizeForSniff(text);
172
+ if (!normalized.startsWith("<")) return false;
173
+ return /<svg(?:\s|>)/i.test(normalized);
174
+ };
175
+ const looksLikeSvg = (sniff) => looksLikeSvgText(sniff.toString("utf8"));
176
+ const sanitizeSvg = (svg) => DOMPurify.sanitize(svg, {
177
+ USE_PROFILES: {
178
+ svg: true,
179
+ svgFilters: true
180
+ }
181
+ });
182
+ const sanitizeSvgProcessor = {
183
+ id: "sanitize-svg",
184
+ maxBytes: MAX_SVG_BYTES,
185
+ match: ({
186
+ sniff
187
+ }) => looksLikeSvg(sniff),
188
+ process: (data) => {
189
+ if (data.length > MAX_SVG_BYTES) {
190
+ throw new Error("svg_too_large");
191
+ }
192
+ const svgText = data.toString("utf8");
193
+ if (!looksLikeSvgText(svgText)) {
194
+ throw new Error("svg_invalid");
195
+ }
196
+ const sanitized = sanitizeSvg(svgText);
197
+ if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {
198
+ throw new Error("svg_sanitize_failed");
199
+ }
200
+ const sanitizedBuffer = Buffer.from(sanitized, "utf8");
201
+ if (sanitizedBuffer.length > MAX_SVG_BYTES) {
202
+ throw new Error("svg_too_large");
203
+ }
204
+ return {
205
+ data: sanitizedBuffer,
206
+ mimeType: "image/svg+xml"
207
+ };
208
+ }
209
+ };
210
+ const processorsById = /* @__PURE__ */ Object.create(null);
211
+ const uploadPostProcessorTaskName = "rb-upload-post-processors";
212
+ const normalizeProcessorId = (value) => typeof value === "string" ? value.trim() : "";
213
+ const normalizeProcessorVersion = (value) => {
214
+ if (typeof value !== "number") return void 0;
215
+ if (!Number.isInteger(value) || value < 1) return void 0;
216
+ return value;
217
+ };
218
+ const registerUploadPostProcessor = (processor) => {
219
+ const normalizedId = normalizeProcessorId(processor.id);
220
+ if (!normalizedId) {
221
+ throw new Error("Upload post processor id is required.");
222
+ }
223
+ const normalizedVersion = normalizeProcessorVersion(processor.version);
224
+ processorsById[normalizedId] = {
225
+ ...processor,
226
+ id: normalizedId,
227
+ ...normalizedVersion ? {
228
+ version: normalizedVersion
229
+ } : {}
230
+ };
231
+ };
232
+ const registerUploadProcessor = registerUploadPostProcessor;
233
+ const unregisterUploadPostProcessor = (id) => {
234
+ const normalizedId = normalizeProcessorId(id);
235
+ if (!normalizedId) return;
236
+ delete processorsById[normalizedId];
237
+ };
238
+ const unregisterUploadProcessor = unregisterUploadPostProcessor;
239
+ const clearUploadPostProcessors = () => {
240
+ for (const id of Object.keys(processorsById)) {
241
+ delete processorsById[id];
242
+ }
243
+ };
244
+ const clearUploadProcessors = clearUploadPostProcessors;
245
+ const getUploadPostProcessors = () => Object.values(processorsById);
246
+ const getUploadProcessors = getUploadPostProcessors;
247
+ const runUploadPostProcessors = async (ctx) => {
248
+ const processors = getUploadPostProcessors();
249
+ if (!processors.length) return;
250
+ for (const processor of processors) {
251
+ if (processor.match) {
252
+ try {
253
+ if (!processor.match(ctx)) continue;
254
+ } catch (error) {
255
+ console.error("Upload post processor failed", {
256
+ processorId: processor.id,
257
+ processorVersion: processor.version,
258
+ tenantId: ctx.tenantId,
259
+ uploadId: ctx.uploadId,
260
+ fileId: ctx.fileId,
261
+ stage: "match",
262
+ error
263
+ });
264
+ continue;
265
+ }
266
+ }
267
+ try {
268
+ await processor.process(ctx);
269
+ } catch (error) {
270
+ console.error("Upload post processor failed", {
271
+ processorId: processor.id,
272
+ processorVersion: processor.version,
273
+ tenantId: ctx.tenantId,
274
+ uploadId: ctx.uploadId,
275
+ fileId: ctx.fileId,
276
+ stage: "process",
277
+ error
278
+ });
279
+ }
280
+ }
281
+ };
282
+ queue.registerTask(uploadPostProcessorTaskName, async (payload) => {
283
+ await runUploadPostProcessors(payload);
284
+ });
285
+ const enqueueUploadPostProcessors = async (ctx) => {
286
+ if (getUploadPostProcessors().length === 0) return;
287
+ await queue.add(uploadPostProcessorTaskName, ctx, {
288
+ attempts: 3,
289
+ backoff: {
290
+ type: "exponential",
291
+ delay: 5e3
292
+ },
293
+ removeOnComplete: true,
294
+ removeOnFail: false
295
+ });
296
+ };
297
+ const DEFAULT_CHUNK_SIZE_BYTES = 5 * 1024 * 1024;
298
+ const MAX_CHUNK_SIZE_BYTES = 15 * 1024 * 1024;
299
+ const DEFAULT_MAX_CLIENT_BYTES_PER_SECOND = 10 * 1024 * 1024;
300
+ const DEFAULT_SESSION_TTL_S = 60 * 60 * 24;
301
+ const ensuredIndexDbNames = /* @__PURE__ */ new Set();
302
+ const parseOptionalPositiveInt = (rawValue) => {
303
+ if (typeof rawValue !== "string") return null;
304
+ const normalized = rawValue.trim();
305
+ if (!normalized) return null;
306
+ const parsed = Number(normalized);
307
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
308
+ return Math.floor(parsed);
309
+ };
310
+ const getChunkSizeBytes = () => {
311
+ const configured = parseOptionalPositiveInt(process.env.RB_UPLOAD_CHUNK_SIZE_BYTES);
312
+ const resolved = configured ?? DEFAULT_CHUNK_SIZE_BYTES;
313
+ return Math.min(MAX_CHUNK_SIZE_BYTES, resolved);
314
+ };
315
+ const getMaxClientUploadBytesPerSecond = () => {
316
+ const configured = parseOptionalPositiveInt(process.env.RB_UPLOAD_MAX_CLIENT_BYTES_PER_SECOND);
317
+ return configured ?? DEFAULT_MAX_CLIENT_BYTES_PER_SECOND;
318
+ };
319
+ const getSessionTtlMs = () => {
320
+ const ttlSeconds = parseOptionalPositiveInt(process.env.RB_UPLOAD_SESSION_TTL_S) ?? DEFAULT_SESSION_TTL_S;
321
+ return ttlSeconds * 1e3;
322
+ };
323
+ const getRawBodyLimitBytes = (chunkSizeBytes) => chunkSizeBytes + 1024 * 1024;
324
+ const getBucketName = () => (process.env.RB_FILESYSTEM_BUCKET_NAME ?? "").trim() || "fs";
325
+ const getUserId = (ctx) => {
326
+ const raw = ctx.req.session?.user?.id;
327
+ if (typeof raw !== "string") return null;
328
+ const normalized = raw.trim();
329
+ return normalized ? normalized : null;
330
+ };
331
+ const getTenantId = (ctx) => {
332
+ const rawSession = ctx.req.session?.user?.currentTenantId;
333
+ const sessionTenantId = typeof rawSession === "string" ? rawSession.trim() : "";
334
+ return sessionTenantId || null;
335
+ };
336
+ const computeSha256Hex = (data) => createHash("sha256").update(data).digest("hex");
337
+ const normalizeSha256Hex = (value) => value.trim().toLowerCase();
338
+ const getModelCtx = (_ctx, tenantId, ability) => ({
339
+ req: {
340
+ session: {
341
+ user: {
342
+ currentTenantId: tenantId
343
+ }
344
+ }
345
+ },
346
+ ability
347
+ });
348
+ const toBufferPayload = (payload) => {
349
+ if (Buffer.isBuffer(payload)) return payload;
350
+ if (payload instanceof Uint8Array) return Buffer.from(payload);
351
+ return null;
352
+ };
353
+ const ensureUploadIndexes = async (UploadSession, UploadChunk) => {
354
+ const dbName = String(UploadSession?.db?.name ?? "");
355
+ if (dbName && ensuredIndexDbNames.has(dbName)) return;
356
+ await Promise.all([UploadSession.createIndexes(), UploadChunk.createIndexes()]);
357
+ if (dbName) ensuredIndexDbNames.add(dbName);
358
+ };
359
+ const normalizeUploadKey = (raw) => {
360
+ if (typeof raw !== "string") return null;
361
+ const normalized = raw.trim();
362
+ return normalized ? normalized : null;
363
+ };
364
+ const getUploadKeyHash = (ctx) => {
365
+ const uploadKey = normalizeUploadKey(ctx.req.get("X-Upload-Key"));
366
+ if (!uploadKey) return null;
367
+ return computeSha256Hex(Buffer.from(uploadKey));
368
+ };
369
+ const buildUploadsAbility = (ctx, tenantId) => {
370
+ const uploadKeyHash = getUploadKeyHash(ctx);
371
+ const claims = uploadKeyHash ? {
372
+ uploadKeyHash
373
+ } : void 0;
374
+ return buildAbilityFromSession({
375
+ tenantId,
376
+ session: ctx.req.session,
377
+ claims
378
+ });
379
+ };
380
+ const getUploadSessionAccessQuery = (ability, action) => getAccessibleByQuery(ability, action, "RBUploadSession");
381
+ const IMAGE_VARIANTS_PROCESSOR_ID = "rb-image-variants";
382
+ const IMAGE_VARIANTS_VERSION = 1;
383
+ const IMAGE_VARIANT_WIDTHS = [320, 640, 960, 1600];
384
+ const IMAGE_VARIANT_QUALITY = 82;
385
+ const IMAGE_VARIANT_MIME_TYPE = "image/webp";
386
+ const IMAGE_VARIANT_FORMAT = "webp";
387
+ const IMAGE_VARIANT_MAX_INPUT_BYTES = 32 * 1024 * 1024;
388
+ const IMAGE_VARIANT_MAX_INPUT_PIXELS = 128 * 1024 * 1024;
389
+ const supportedImageMimeTypes = /* @__PURE__ */ new Set(["image/avif", "image/heic", "image/heif", "image/jpeg", "image/png", "image/webp"]);
390
+ const normalizeMimeType = (value) => value.trim().toLowerCase();
391
+ const toObjectId = (value) => {
392
+ try {
393
+ return new ObjectId(value);
394
+ } catch {
395
+ return null;
396
+ }
397
+ };
398
+ const readGridFsFile = async (bucket, fileId) => new Promise((resolve, reject) => {
399
+ const chunks = [];
400
+ const stream = bucket.openDownloadStream(fileId);
401
+ stream.on("data", (chunk) => {
402
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
403
+ });
404
+ stream.once("error", reject);
405
+ stream.once("end", () => resolve(Buffer.concat(chunks)));
406
+ });
407
+ const writeGridFsFile = async (bucket, filename, data, metadata) => new Promise((resolve, reject) => {
408
+ const stream = bucket.openUploadStream(filename, {
409
+ metadata
410
+ });
411
+ stream.once("error", reject);
412
+ stream.once("finish", () => resolve(String(stream.id ?? "")));
413
+ stream.end(data);
414
+ });
415
+ registerUploadPostProcessor({
416
+ id: IMAGE_VARIANTS_PROCESSOR_ID,
417
+ version: IMAGE_VARIANTS_VERSION,
418
+ match: ({
419
+ mimeType,
420
+ metadata
421
+ }) => {
422
+ if (typeof metadata.variantOf === "string") return false;
423
+ return supportedImageMimeTypes.has(normalizeMimeType(mimeType));
424
+ },
425
+ process: async ({
426
+ tenantId,
427
+ fileId,
428
+ filename,
429
+ isPublic,
430
+ userId,
431
+ ownerKeyHash,
432
+ totalSize
433
+ }) => {
434
+ const originalObjectId = toObjectId(fileId);
435
+ if (!originalObjectId) return;
436
+ const fsDb = await getTenantFilesystemDb(tenantId);
437
+ const nativeDb = fsDb.db;
438
+ if (!nativeDb) return;
439
+ const bucketName = getBucketName();
440
+ const filesCollection = nativeDb.collection(`${bucketName}.files`);
441
+ const existingFile = await filesCollection.findOne({
442
+ _id: originalObjectId
443
+ });
444
+ if (!existingFile) return;
445
+ const existingImageMetadata = existingFile && typeof existingFile.metadata === "object" && existingFile.metadata ? existingFile.metadata.image : null;
446
+ if (existingImageMetadata && typeof existingImageMetadata === "object" && existingImageMetadata.variantsStatus === "done" && existingImageMetadata.variantsVersion === IMAGE_VARIANTS_VERSION) {
447
+ return;
448
+ }
449
+ const markVariantsSkipped = async (reason) => {
450
+ await filesCollection.updateOne({
451
+ _id: originalObjectId
452
+ }, {
453
+ $set: {
454
+ "metadata.image.variantsStatus": "skipped",
455
+ "metadata.image.variantsVersion": IMAGE_VARIANTS_VERSION,
456
+ "metadata.image.variantsSkippedReason": reason
457
+ },
458
+ $unset: {
459
+ "metadata.image.variants": ""
460
+ }
461
+ });
462
+ };
463
+ const existingFileLength = typeof existingFile.length === "number" ? existingFile.length : null;
464
+ const sourceBytes = existingFileLength ?? totalSize;
465
+ if (sourceBytes > IMAGE_VARIANT_MAX_INPUT_BYTES) {
466
+ await markVariantsSkipped("source_too_large");
467
+ return;
468
+ }
469
+ const bucket = new GridFSBucket(nativeDb, {
470
+ bucketName
471
+ });
472
+ const source = await readGridFsFile(bucket, originalObjectId);
473
+ const metadata = await sharp(source, {
474
+ limitInputPixels: IMAGE_VARIANT_MAX_INPUT_PIXELS
475
+ }).metadata();
476
+ if (!metadata.width || !metadata.height) {
477
+ await markVariantsSkipped("missing_dimensions");
478
+ return;
479
+ }
480
+ const targetWidths = IMAGE_VARIANT_WIDTHS.filter((width) => width < metadata.width);
481
+ const variants = {};
482
+ for (const width of targetWidths) {
483
+ const output = await sharp(source, {
484
+ limitInputPixels: IMAGE_VARIANT_MAX_INPUT_PIXELS
485
+ }).rotate().resize({
486
+ width,
487
+ withoutEnlargement: true
488
+ }).webp({
489
+ quality: IMAGE_VARIANT_QUALITY,
490
+ effort: 4
491
+ }).toBuffer({
492
+ resolveWithObject: true
493
+ });
494
+ const variantFileId = await writeGridFsFile(bucket, `${filename}.${width}.${IMAGE_VARIANT_FORMAT}`, output.data, {
495
+ mimeType: IMAGE_VARIANT_MIME_TYPE,
496
+ totalSize: output.data.length,
497
+ variantOf: fileId,
498
+ variantWidth: width,
499
+ variantFormat: IMAGE_VARIANT_FORMAT,
500
+ variantsVersion: IMAGE_VARIANTS_VERSION,
501
+ ...typeof isPublic === "boolean" ? {
502
+ isPublic
503
+ } : {},
504
+ ...userId ? {
505
+ userId
506
+ } : {},
507
+ ...ownerKeyHash ? {
508
+ ownerKeyHash
509
+ } : {}
510
+ });
511
+ if (!variantFileId) continue;
512
+ variants[String(width)] = {
513
+ fileId: variantFileId,
514
+ width: output.info.width,
515
+ height: output.info.height,
516
+ mimeType: IMAGE_VARIANT_MIME_TYPE,
517
+ size: output.data.length
518
+ };
519
+ }
520
+ await filesCollection.updateOne({
521
+ _id: originalObjectId
522
+ }, {
523
+ $set: {
524
+ "metadata.image": {
525
+ width: metadata.width,
526
+ height: metadata.height,
527
+ format: metadata.format,
528
+ variantsStatus: Object.keys(variants).length ? "done" : "skipped",
529
+ variantsVersion: IMAGE_VARIANTS_VERSION,
530
+ variants
531
+ }
532
+ }
533
+ });
534
+ }
535
+ });
536
+ const routes = Object.entries({
537
+ .../* @__PURE__ */ Object.assign({ "./api/file-uploads/handler.ts": () => import("./handler-Da2KGCRq.js"), "./api/files/handler.ts": () => import("./handler-Cn5I8j8k.js") })
538
+ }).reduce((acc, [path, mod]) => {
539
+ acc[path.replace("./api/", "@rpcbase/server/uploads/api/")] = mod;
540
+ return acc;
541
+ }, {});
542
+ export {
543
+ unregisterUploadPostProcessor as A,
544
+ unregisterUploadProcessor as B,
545
+ uploadPostProcessorTaskName as C,
546
+ getUploadSessionAccessQuery as a,
547
+ buildUploadsAbility as b,
548
+ convertHeifToWebpProcessor as c,
549
+ getBucketName as d,
550
+ ensureUploadIndexes as e,
551
+ enqueueUploadPostProcessors as f,
552
+ getTenantId as g,
553
+ getModelCtx as h,
554
+ getUserId as i,
555
+ getChunkSizeBytes as j,
556
+ getSessionTtlMs as k,
557
+ computeSha256Hex as l,
558
+ getMaxClientUploadBytesPerSecond as m,
559
+ normalizeSha256Hex as n,
560
+ getRawBodyLimitBytes as o,
561
+ getUploadKeyHash as p,
562
+ clearUploadPostProcessors as q,
563
+ clearUploadProcessors as r,
564
+ sanitizeSvgProcessor as s,
565
+ toBufferPayload as t,
566
+ getUploadPostProcessors as u,
567
+ getUploadProcessors as v,
568
+ registerUploadPostProcessor as w,
569
+ registerUploadProcessor as x,
570
+ routes as y,
571
+ runUploadPostProcessors as z
572
+ };
573
+ //# sourceMappingURL=uploads-BAxHzidK.js.map