@rpcbase/server 0.479.0 → 0.481.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/applyRouteLoaders.d.ts.map +1 -1
- package/dist/{handler-BOTZftAB.js → handler-BA2YiqnG.js} +199 -16
- package/dist/{handler-Cl-0-832.js → handler-Cohj3cz3.js} +54 -1
- package/dist/index.js +32 -4
- package/dist/initServer.d.ts.map +1 -1
- package/dist/{shared-UGuDRAKK.js → shared-BJomDDWK.js} +1 -0
- package/dist/uploads/api/file-uploads/handlers/completeUpload.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/handlers/initUpload.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/index.d.ts +1 -0
- package/dist/uploads/api/file-uploads/index.d.ts.map +1 -1
- package/dist/uploads/api/file-uploads/processors/index.d.ts +25 -0
- package/dist/uploads/api/file-uploads/processors/index.d.ts.map +1 -0
- package/dist/uploads/api/file-uploads/processors/sanitizeSvg.d.ts +5 -0
- package/dist/uploads/api/file-uploads/processors/sanitizeSvg.d.ts.map +1 -0
- package/dist/uploads/api/files/handlers/deleteFile.d.ts.map +1 -1
- package/dist/uploads/api/files/handlers/getFile.d.ts +2 -1
- package/dist/uploads/api/files/handlers/getFile.d.ts.map +1 -1
- package/dist/uploads.js +1 -1
- package/package.json +5 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAC/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"applyRouteLoaders.d.ts","sourceRoot":"","sources":["../src/applyRouteLoaders.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAA;AAC/B,OAAO,EACL,oBAAoB,EAMrB,MAAM,iBAAiB,CAAA;AA0GxB,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D,gBAAgB,CAAC,EAAE,QAAQ,CAAA;IAC3B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAClC,CAAA;AAED,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,UAAU,EAAE,GAAG,EAAE,GAChB,OAAO,CAAC,yBAAyB,CAAC,CAwLpC"}
|
|
@@ -1,8 +1,77 @@
|
|
|
1
1
|
import { loadModel, getTenantFilesystemDb } from "@rpcbase/db";
|
|
2
2
|
import { GridFSBucket, ObjectId } from "mongodb";
|
|
3
|
-
import {
|
|
3
|
+
import { JSDOM } from "jsdom";
|
|
4
|
+
import createDOMPurify from "dompurify";
|
|
5
|
+
import { g as getTenantId, a as getModelCtx, b as buildUploadsAbility, c as getUploadSessionAccessQuery, e as ensureUploadIndexes, d as getBucketName, f as getUserId, h as getChunkSizeBytes, i as getSessionTtlMs, j as computeSha256Hex, t as toBufferPayload, n as normalizeSha256Hex, k as getMaxClientUploadBytesPerSecond, l as getRawBodyLimitBytes } from "./shared-BJomDDWK.js";
|
|
4
6
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { o as object, n as number,
|
|
7
|
+
import { o as object, n as number, b as boolean, s as string, a as array, _ as _enum } from "./schemas-D5T9tDtI.js";
|
|
8
|
+
const MAX_SVG_BYTES = 128 * 1024;
|
|
9
|
+
const window = new JSDOM("").window;
|
|
10
|
+
const DOMPurify = createDOMPurify(window);
|
|
11
|
+
const normalizeForSniff = (raw) => raw.replace(/^\uFEFF/, "").trimStart();
|
|
12
|
+
const looksLikeSvgText = (text) => {
|
|
13
|
+
const normalized = normalizeForSniff(text);
|
|
14
|
+
if (!normalized.startsWith("<")) return false;
|
|
15
|
+
return /<svg(?:\s|>)/i.test(normalized);
|
|
16
|
+
};
|
|
17
|
+
const looksLikeSvg = (sniff) => looksLikeSvgText(sniff.toString("utf8"));
|
|
18
|
+
const sanitizeSvg = (svg) => DOMPurify.sanitize(svg, {
|
|
19
|
+
USE_PROFILES: { svg: true, svgFilters: true }
|
|
20
|
+
});
|
|
21
|
+
const sanitizeSvgProcessor = {
|
|
22
|
+
id: "sanitize-svg",
|
|
23
|
+
maxBytes: MAX_SVG_BYTES,
|
|
24
|
+
match: ({ sniff }) => looksLikeSvg(sniff),
|
|
25
|
+
process: (data) => {
|
|
26
|
+
if (data.length > MAX_SVG_BYTES) {
|
|
27
|
+
throw new Error("svg_too_large");
|
|
28
|
+
}
|
|
29
|
+
const svgText = data.toString("utf8");
|
|
30
|
+
if (!looksLikeSvgText(svgText)) {
|
|
31
|
+
throw new Error("svg_invalid");
|
|
32
|
+
}
|
|
33
|
+
const sanitized = sanitizeSvg(svgText);
|
|
34
|
+
if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {
|
|
35
|
+
throw new Error("svg_sanitize_failed");
|
|
36
|
+
}
|
|
37
|
+
const sanitizedBuffer = Buffer.from(sanitized, "utf8");
|
|
38
|
+
if (sanitizedBuffer.length > MAX_SVG_BYTES) {
|
|
39
|
+
throw new Error("svg_too_large");
|
|
40
|
+
}
|
|
41
|
+
return { data: sanitizedBuffer, mimeType: "image/svg+xml" };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const uploadProcessors = Object.freeze([sanitizeSvgProcessor]);
|
|
45
|
+
const getMaxUploadProcessorBytes = () => uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0);
|
|
46
|
+
const selectUploadProcessors = (ctx) => uploadProcessors.filter((processor) => processor.match(ctx));
|
|
47
|
+
const applyUploadProcessors = async (data, ctx) => {
|
|
48
|
+
let currentData = data;
|
|
49
|
+
let currentMimeType = ctx.clientMimeType;
|
|
50
|
+
const applied = [];
|
|
51
|
+
for (const processor of uploadProcessors) {
|
|
52
|
+
const processorCtx = {
|
|
53
|
+
filename: ctx.filename,
|
|
54
|
+
clientMimeType: currentMimeType,
|
|
55
|
+
totalSize: currentData.length,
|
|
56
|
+
sniff: currentData
|
|
57
|
+
};
|
|
58
|
+
if (!processor.match(processorCtx)) continue;
|
|
59
|
+
if (currentData.length > processor.maxBytes) {
|
|
60
|
+
throw new Error("processor_input_too_large");
|
|
61
|
+
}
|
|
62
|
+
const result = await processor.process(currentData, processorCtx);
|
|
63
|
+
currentData = result.data;
|
|
64
|
+
if (typeof result.mimeType === "string" && result.mimeType.trim()) {
|
|
65
|
+
currentMimeType = result.mimeType.trim();
|
|
66
|
+
}
|
|
67
|
+
applied.push(processor.id);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
data: currentData,
|
|
71
|
+
mimeType: currentMimeType,
|
|
72
|
+
applied
|
|
73
|
+
};
|
|
74
|
+
};
|
|
6
75
|
const waitForStreamFinished = async (stream) => new Promise((resolve, reject) => {
|
|
7
76
|
stream.once("finish", resolve);
|
|
8
77
|
stream.once("error", reject);
|
|
@@ -94,24 +163,70 @@ const completeUpload = async (_payload, ctx) => {
|
|
|
94
163
|
const bucketName = getBucketName();
|
|
95
164
|
const bucket = new GridFSBucket(nativeDb, { bucketName });
|
|
96
165
|
const lockedUserId = typeof locked.userId === "string" ? locked.userId : void 0;
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
totalSize: locked.totalSize,
|
|
103
|
-
...lockedUserId ? { userId: lockedUserId } : {}
|
|
104
|
-
}
|
|
105
|
-
});
|
|
166
|
+
const maxProcessorBytes = getMaxUploadProcessorBytes();
|
|
167
|
+
const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes;
|
|
168
|
+
const declaredMimeType = locked.mimeType.trim().toLowerCase();
|
|
169
|
+
const declaredSvg = declaredMimeType === "image/svg+xml" || locked.filename.trim().toLowerCase().endsWith(".svg");
|
|
170
|
+
let uploadStream = null;
|
|
106
171
|
try {
|
|
172
|
+
if (!shouldBufferForProcessing && declaredSvg) {
|
|
173
|
+
throw new Error("svg_too_large");
|
|
174
|
+
}
|
|
107
175
|
const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor();
|
|
108
176
|
let expectedIndex = 0;
|
|
177
|
+
const chunks = [];
|
|
178
|
+
let bufferedBytes = 0;
|
|
179
|
+
const pendingChunks = [];
|
|
180
|
+
const sniffParts = [];
|
|
181
|
+
let sniffBytes = 0;
|
|
109
182
|
try {
|
|
110
183
|
for await (const chunkDoc of cursor) {
|
|
111
184
|
if (chunkDoc.index !== expectedIndex) {
|
|
112
185
|
throw new Error("missing_chunks");
|
|
113
186
|
}
|
|
114
|
-
|
|
187
|
+
const chunk = chunkDoc.data;
|
|
188
|
+
if (shouldBufferForProcessing) {
|
|
189
|
+
chunks.push(chunk);
|
|
190
|
+
bufferedBytes += chunk.length;
|
|
191
|
+
} else if (!uploadStream) {
|
|
192
|
+
pendingChunks.push(chunk);
|
|
193
|
+
if (sniffBytes < maxProcessorBytes) {
|
|
194
|
+
const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes));
|
|
195
|
+
if (slice.length) {
|
|
196
|
+
sniffParts.push(slice);
|
|
197
|
+
sniffBytes += slice.length;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (sniffBytes >= maxProcessorBytes) {
|
|
201
|
+
const sniff = Buffer.concat(sniffParts, sniffBytes);
|
|
202
|
+
const processors = selectUploadProcessors({
|
|
203
|
+
filename: locked.filename,
|
|
204
|
+
clientMimeType: locked.mimeType,
|
|
205
|
+
totalSize: locked.totalSize,
|
|
206
|
+
sniff
|
|
207
|
+
});
|
|
208
|
+
if (processors.length) {
|
|
209
|
+
throw new Error("svg_too_large");
|
|
210
|
+
}
|
|
211
|
+
uploadStream = bucket.openUploadStream(locked.filename, {
|
|
212
|
+
metadata: {
|
|
213
|
+
uploadId,
|
|
214
|
+
tenantId,
|
|
215
|
+
mimeType: locked.mimeType,
|
|
216
|
+
totalSize: locked.totalSize,
|
|
217
|
+
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
218
|
+
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
219
|
+
...lockedUserId ? { userId: lockedUserId } : {}
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
for (const pending of pendingChunks) {
|
|
223
|
+
await writeToStream(uploadStream, pending);
|
|
224
|
+
}
|
|
225
|
+
pendingChunks.length = 0;
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
await writeToStream(uploadStream, chunk);
|
|
229
|
+
}
|
|
115
230
|
expectedIndex += 1;
|
|
116
231
|
}
|
|
117
232
|
} finally {
|
|
@@ -123,9 +238,59 @@ const completeUpload = async (_payload, ctx) => {
|
|
|
123
238
|
if (expectedIndex !== locked.chunksTotal) {
|
|
124
239
|
throw new Error("missing_chunks");
|
|
125
240
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
241
|
+
if (shouldBufferForProcessing) {
|
|
242
|
+
const assembled = Buffer.concat(chunks, bufferedBytes);
|
|
243
|
+
const { data: processed, mimeType: processedMimeType, applied } = await applyUploadProcessors(assembled, {
|
|
244
|
+
filename: locked.filename,
|
|
245
|
+
clientMimeType: locked.mimeType
|
|
246
|
+
});
|
|
247
|
+
uploadStream = bucket.openUploadStream(locked.filename, {
|
|
248
|
+
metadata: {
|
|
249
|
+
uploadId,
|
|
250
|
+
tenantId,
|
|
251
|
+
mimeType: processedMimeType,
|
|
252
|
+
totalSize: locked.totalSize,
|
|
253
|
+
...applied.length ? { processors: applied, processedSize: processed.length } : {},
|
|
254
|
+
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
255
|
+
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
256
|
+
...lockedUserId ? { userId: lockedUserId } : {}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
const finished = waitForStreamFinished(uploadStream);
|
|
260
|
+
uploadStream.end(processed);
|
|
261
|
+
await finished;
|
|
262
|
+
} else {
|
|
263
|
+
if (!uploadStream) {
|
|
264
|
+
const sniff = Buffer.concat(sniffParts, sniffBytes);
|
|
265
|
+
const processors = selectUploadProcessors({
|
|
266
|
+
filename: locked.filename,
|
|
267
|
+
clientMimeType: locked.mimeType,
|
|
268
|
+
totalSize: locked.totalSize,
|
|
269
|
+
sniff
|
|
270
|
+
});
|
|
271
|
+
if (processors.length) {
|
|
272
|
+
throw new Error("svg_too_large");
|
|
273
|
+
}
|
|
274
|
+
uploadStream = bucket.openUploadStream(locked.filename, {
|
|
275
|
+
metadata: {
|
|
276
|
+
uploadId,
|
|
277
|
+
tenantId,
|
|
278
|
+
mimeType: locked.mimeType,
|
|
279
|
+
totalSize: locked.totalSize,
|
|
280
|
+
...typeof locked.isPublic === "boolean" ? { isPublic: locked.isPublic } : {},
|
|
281
|
+
...typeof locked.ownerKeyHash === "string" ? { ownerKeyHash: locked.ownerKeyHash } : {},
|
|
282
|
+
...lockedUserId ? { userId: lockedUserId } : {}
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
for (const pending of pendingChunks) {
|
|
286
|
+
await writeToStream(uploadStream, pending);
|
|
287
|
+
}
|
|
288
|
+
pendingChunks.length = 0;
|
|
289
|
+
}
|
|
290
|
+
const finished = waitForStreamFinished(uploadStream);
|
|
291
|
+
uploadStream.end();
|
|
292
|
+
await finished;
|
|
293
|
+
}
|
|
129
294
|
const fileId = String(uploadStream.id ?? "");
|
|
130
295
|
if (!fileId) {
|
|
131
296
|
throw new Error("missing_file_id");
|
|
@@ -150,6 +315,22 @@ const completeUpload = async (_payload, ctx) => {
|
|
|
150
315
|
ctx.res.status(409);
|
|
151
316
|
return { ok: false, error: "missing_chunks" };
|
|
152
317
|
}
|
|
318
|
+
if (message === "svg_too_large") {
|
|
319
|
+
await UploadSession.updateOne(
|
|
320
|
+
{ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
|
|
321
|
+
{ $set: { status: "error", error: message } }
|
|
322
|
+
);
|
|
323
|
+
ctx.res.status(413);
|
|
324
|
+
return { ok: false, error: message };
|
|
325
|
+
}
|
|
326
|
+
if (message === "svg_invalid" || message === "svg_sanitize_failed") {
|
|
327
|
+
await UploadSession.updateOne(
|
|
328
|
+
{ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
|
|
329
|
+
{ $set: { status: "error", error: message } }
|
|
330
|
+
);
|
|
331
|
+
ctx.res.status(400);
|
|
332
|
+
return { ok: false, error: message };
|
|
333
|
+
}
|
|
153
334
|
await UploadSession.updateOne(
|
|
154
335
|
{ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, "update")] },
|
|
155
336
|
{ $set: { status: "error", error: message } }
|
|
@@ -205,6 +386,7 @@ const CompleteRoute = "/api/rb/file-uploads/:uploadId/complete";
|
|
|
205
386
|
const initRequestSchema = object({
|
|
206
387
|
filename: string().min(1),
|
|
207
388
|
mimeType: string().min(1),
|
|
389
|
+
isPublic: boolean().optional(),
|
|
208
390
|
totalSize: number().int().min(1)
|
|
209
391
|
});
|
|
210
392
|
object({
|
|
@@ -242,7 +424,7 @@ const initUpload = async (payload, ctx) => {
|
|
|
242
424
|
return { ok: false, error: "invalid_payload" };
|
|
243
425
|
}
|
|
244
426
|
const chunkSize = getChunkSizeBytes();
|
|
245
|
-
const { filename, mimeType, totalSize } = parsed.data;
|
|
427
|
+
const { filename, mimeType, totalSize, isPublic } = parsed.data;
|
|
246
428
|
const chunksTotal = Math.ceil(totalSize / chunkSize);
|
|
247
429
|
const modelCtx = getModelCtx(ctx, tenantId);
|
|
248
430
|
const [UploadSession, UploadChunk] = await Promise.all([
|
|
@@ -261,6 +443,7 @@ const initUpload = async (payload, ctx) => {
|
|
|
261
443
|
...ownerKeyHash ? { ownerKeyHash } : {},
|
|
262
444
|
filename,
|
|
263
445
|
mimeType,
|
|
446
|
+
...typeof isPublic === "boolean" ? { isPublic } : {},
|
|
264
447
|
totalSize,
|
|
265
448
|
chunkSize,
|
|
266
449
|
chunksTotal,
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { getTenantFilesystemDb } from "@rpcbase/db";
|
|
2
2
|
import { ObjectId, GridFSBucket } from "mongodb";
|
|
3
|
-
import { g as getTenantId, d as getBucketName } from "./shared-
|
|
3
|
+
import { g as getTenantId, d as getBucketName, f as getUserId, m as getUploadKeyHash } from "./shared-BJomDDWK.js";
|
|
4
|
+
const resolveHeaderString$1 = (value) => {
|
|
5
|
+
if (typeof value !== "string") return null;
|
|
6
|
+
const normalized = value.trim();
|
|
7
|
+
return normalized ? normalized : null;
|
|
8
|
+
};
|
|
4
9
|
const deleteFile = async (_payload, ctx) => {
|
|
5
10
|
const tenantId = getTenantId(ctx);
|
|
6
11
|
if (!tenantId) {
|
|
@@ -23,6 +28,25 @@ const deleteFile = async (_payload, ctx) => {
|
|
|
23
28
|
}
|
|
24
29
|
const bucketName = getBucketName();
|
|
25
30
|
const bucket = new GridFSBucket(nativeDb, { bucketName });
|
|
31
|
+
const userId = getUserId(ctx);
|
|
32
|
+
const uploadKeyHash = getUploadKeyHash(ctx);
|
|
33
|
+
if (!userId && !uploadKeyHash) {
|
|
34
|
+
ctx.res.status(401);
|
|
35
|
+
return { ok: false, error: "unauthorized" };
|
|
36
|
+
}
|
|
37
|
+
const [file] = await bucket.find({ _id: fileObjectId }).limit(1).toArray();
|
|
38
|
+
if (!file) {
|
|
39
|
+
ctx.res.status(204);
|
|
40
|
+
return { ok: true };
|
|
41
|
+
}
|
|
42
|
+
const metadataUserId = resolveHeaderString$1(file?.metadata?.userId);
|
|
43
|
+
const ownerKeyHash = resolveHeaderString$1(file?.metadata?.ownerKeyHash);
|
|
44
|
+
const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId);
|
|
45
|
+
const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash);
|
|
46
|
+
if (!authorizedByUser && !authorizedByKey) {
|
|
47
|
+
ctx.res.status(401);
|
|
48
|
+
return { ok: false, error: "unauthorized" };
|
|
49
|
+
}
|
|
26
50
|
try {
|
|
27
51
|
await bucket.delete(fileObjectId);
|
|
28
52
|
} catch (error) {
|
|
@@ -40,6 +64,15 @@ const resolveHeaderString = (value) => {
|
|
|
40
64
|
const normalized = value.trim();
|
|
41
65
|
return normalized ? normalized : null;
|
|
42
66
|
};
|
|
67
|
+
const resolveHeaderBoolean = (value) => {
|
|
68
|
+
if (typeof value === "boolean") return value;
|
|
69
|
+
if (typeof value !== "string") return null;
|
|
70
|
+
const normalized = value.trim().toLowerCase();
|
|
71
|
+
if (!normalized) return null;
|
|
72
|
+
if (normalized === "true") return true;
|
|
73
|
+
if (normalized === "false") return false;
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
43
76
|
const escapeHeaderFilename = (filename) => filename.replace(/[\\"]/g, "_");
|
|
44
77
|
const getFile = async (_payload, ctx) => {
|
|
45
78
|
const tenantId = getTenantId(ctx);
|
|
@@ -68,6 +101,19 @@ const getFile = async (_payload, ctx) => {
|
|
|
68
101
|
ctx.res.status(404).end();
|
|
69
102
|
return {};
|
|
70
103
|
}
|
|
104
|
+
const isPublic = resolveHeaderBoolean(file?.metadata?.isPublic) ?? false;
|
|
105
|
+
if (!isPublic) {
|
|
106
|
+
const userId = getUserId(ctx);
|
|
107
|
+
const uploadKeyHash = getUploadKeyHash(ctx);
|
|
108
|
+
const metadataUserId = resolveHeaderString(file?.metadata?.userId);
|
|
109
|
+
const ownerKeyHash = resolveHeaderString(file?.metadata?.ownerKeyHash);
|
|
110
|
+
const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId);
|
|
111
|
+
const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash);
|
|
112
|
+
if (!authorizedByUser && !authorizedByKey) {
|
|
113
|
+
ctx.res.status(401).end();
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
71
117
|
const mimeTypeFromMetadata = resolveHeaderString(file?.metadata?.mimeType);
|
|
72
118
|
const mimeType = mimeTypeFromMetadata ?? "application/octet-stream";
|
|
73
119
|
const filenameFromDb = resolveHeaderString(file?.filename);
|
|
@@ -95,6 +141,13 @@ const getFile = async (_payload, ctx) => {
|
|
|
95
141
|
ctx.res.setHeader("Content-Disposition", `inline; filename="${filenameSafe}"`);
|
|
96
142
|
ctx.res.setHeader("Cache-Control", cacheControl);
|
|
97
143
|
ctx.res.setHeader("ETag", etag);
|
|
144
|
+
ctx.res.setHeader("X-Content-Type-Options", "nosniff");
|
|
145
|
+
if (mimeType === "image/svg+xml") {
|
|
146
|
+
ctx.res.setHeader(
|
|
147
|
+
"Content-Security-Policy",
|
|
148
|
+
"default-src 'none'; style-src 'unsafe-inline'; sandbox; base-uri 'none'; form-action 'none'"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
98
151
|
ctx.res.flushHeaders();
|
|
99
152
|
if (ctx.req.method === "HEAD") {
|
|
100
153
|
ctx.res.end();
|
package/dist/index.js
CHANGED
|
@@ -3429,8 +3429,11 @@ const getMongoUrl = (serverEnv) => {
|
|
|
3429
3429
|
}
|
|
3430
3430
|
if (serverEnv.DB_PORT) {
|
|
3431
3431
|
const host = serverEnv.DB_HOST ?? "localhost";
|
|
3432
|
-
const
|
|
3433
|
-
|
|
3432
|
+
const appName = serverEnv.APP_NAME?.trim();
|
|
3433
|
+
if (!appName) {
|
|
3434
|
+
throw new Error("Missing APP_NAME (required to build MongoDB session store DB name)");
|
|
3435
|
+
}
|
|
3436
|
+
return `mongodb://${host}:${serverEnv.DB_PORT}/${appName}-sessions`;
|
|
3434
3437
|
}
|
|
3435
3438
|
return void 0;
|
|
3436
3439
|
};
|
|
@@ -3781,6 +3784,22 @@ const isRedirectResponse = (value) => {
|
|
|
3781
3784
|
if (!isResponseLike(value)) return false;
|
|
3782
3785
|
return value.status >= 300 && value.status < 400 && Boolean(value.headers.get("Location"));
|
|
3783
3786
|
};
|
|
3787
|
+
const getRouteHandleStatusCode = (route) => {
|
|
3788
|
+
if (!route) return void 0;
|
|
3789
|
+
const handle = route.handle;
|
|
3790
|
+
if (!handle) return void 0;
|
|
3791
|
+
const candidate = handle.statusCode ?? handle.status ?? handle.httpStatus;
|
|
3792
|
+
if (typeof candidate === "number") return candidate;
|
|
3793
|
+
return void 0;
|
|
3794
|
+
};
|
|
3795
|
+
const isNotFoundFallbackRoute = (route) => {
|
|
3796
|
+
if (!route) return false;
|
|
3797
|
+
if (route.path !== "*") return false;
|
|
3798
|
+
if (route.loader) return false;
|
|
3799
|
+
if (route.action) return false;
|
|
3800
|
+
if (route.children?.length) return false;
|
|
3801
|
+
return true;
|
|
3802
|
+
};
|
|
3784
3803
|
async function applyRouteLoaders(req, dataRoutes) {
|
|
3785
3804
|
const baseUrl = `${req.protocol}://${req.get("host")}`;
|
|
3786
3805
|
const url = new URL(req.originalUrl, baseUrl);
|
|
@@ -3792,8 +3811,8 @@ async function applyRouteLoaders(req, dataRoutes) {
|
|
|
3792
3811
|
loaderHeaders: {},
|
|
3793
3812
|
actionHeaders: {}
|
|
3794
3813
|
};
|
|
3795
|
-
const matches = matchRoutes(dataRoutes, location)
|
|
3796
|
-
if (!matches) {
|
|
3814
|
+
const matches = matchRoutes(dataRoutes, location);
|
|
3815
|
+
if (!matches || matches.length === 0) {
|
|
3797
3816
|
const error = {
|
|
3798
3817
|
status: 404,
|
|
3799
3818
|
message: `No route matches URL: ${req.originalUrl}`
|
|
@@ -3920,6 +3939,15 @@ async function applyRouteLoaders(req, dataRoutes) {
|
|
|
3920
3939
|
statusCode = 500;
|
|
3921
3940
|
}
|
|
3922
3941
|
}
|
|
3942
|
+
if (!errors && statusCode === 200) {
|
|
3943
|
+
const leafRoute = matches.at(-1)?.route;
|
|
3944
|
+
const handleStatusCode = getRouteHandleStatusCode(leafRoute);
|
|
3945
|
+
if (typeof handleStatusCode === "number") {
|
|
3946
|
+
statusCode = handleStatusCode;
|
|
3947
|
+
} else if (isNotFoundFallbackRoute(leafRoute)) {
|
|
3948
|
+
statusCode = NOT_FOUND_STATUS;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3923
3951
|
return {
|
|
3924
3952
|
...baseContext,
|
|
3925
3953
|
matches,
|
package/dist/initServer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initServer.d.ts","sourceRoot":"","sources":["../src/initServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyBrC,KAAK,SAAS,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"initServer.d.ts","sourceRoot":"","sources":["../src/initServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyBrC,KAAK,SAAS,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAA;AA0FtD,eAAO,MAAM,UAAU,GAAU,KAAK,WAAW,EAAE,WAAW,SAAS,kBAsEtE,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completeUpload.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/handlers/completeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,OAAO,KAAK,OAAO,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"completeUpload.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/handlers/completeUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,OAAO,KAAK,OAAO,MAAM,UAAU,CAAA;AAEnC,OAAO,EACL,KAAK,WAAW,EASjB,MAAM,WAAW,CAAA;AAiDlB,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,uBAAuB,EAAE,WAAW,CAqR1G,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"initUpload.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/handlers/initUpload.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,OAAO,KAAK,OAAO,MAAM,UAAU,CAAA;AACnC,OAAO,EACL,KAAK,WAAW,EAUjB,MAAM,WAAW,CAAA;AAGlB,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,mBAAmB,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"initUpload.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/handlers/initUpload.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAKzC,OAAO,KAAK,OAAO,MAAM,UAAU,CAAA;AACnC,OAAO,EACL,KAAK,WAAW,EAUjB,MAAM,WAAW,CAAA;AAGlB,eAAO,MAAM,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,mBAAmB,EAAE,WAAW,CA4DvG,CAAA"}
|
|
@@ -6,6 +6,7 @@ export declare const CompleteRoute = "/api/rb/file-uploads/:uploadId/complete";
|
|
|
6
6
|
export declare const initRequestSchema: z.ZodObject<{
|
|
7
7
|
filename: z.ZodString;
|
|
8
8
|
mimeType: z.ZodString;
|
|
9
|
+
isPublic: z.ZodOptional<z.ZodBoolean>;
|
|
9
10
|
totalSize: z.ZodNumber;
|
|
10
11
|
}, z.core.$strip>;
|
|
11
12
|
export type InitRequestPayload = z.infer<typeof initRequestSchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/uploads/api/file-uploads/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,SAAS,yBAAyB,CAAA;AAC/C,eAAO,MAAM,UAAU,iDAAiD,CAAA;AACxE,eAAO,MAAM,WAAW,0CAA0C,CAAA;AAClE,eAAO,MAAM,aAAa,4CAA4C,CAAA;AAEtE,eAAO,MAAM,iBAAiB
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/uploads/api/file-uploads/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,SAAS,yBAAyB,CAAA;AAC/C,eAAO,MAAM,UAAU,iDAAiD,CAAA;AACxE,eAAO,MAAM,WAAW,0CAA0C,CAAA;AAClE,eAAO,MAAM,aAAa,4CAA4C,CAAA;AAEtE,eAAO,MAAM,iBAAiB;;;;;iBAK5B,CAAA;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAA;AAElE,eAAO,MAAM,kBAAkB;;;;;;;iBAO7B,CAAA;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAEpE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAQ/B,CAAA;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AAExE,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAA;AAEF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type UploadFileProcessorContext = {
|
|
2
|
+
filename: string;
|
|
3
|
+
clientMimeType: string;
|
|
4
|
+
totalSize: number;
|
|
5
|
+
sniff: Buffer;
|
|
6
|
+
};
|
|
7
|
+
export type UploadFileProcessorResult = {
|
|
8
|
+
data: Buffer;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
};
|
|
11
|
+
export type UploadFileProcessor = {
|
|
12
|
+
id: string;
|
|
13
|
+
maxBytes: number;
|
|
14
|
+
match: (ctx: UploadFileProcessorContext) => boolean;
|
|
15
|
+
process: (data: Buffer, ctx: UploadFileProcessorContext) => Promise<UploadFileProcessorResult> | UploadFileProcessorResult;
|
|
16
|
+
};
|
|
17
|
+
export declare const uploadProcessors: readonly UploadFileProcessor[];
|
|
18
|
+
export declare const getMaxUploadProcessorBytes: () => number;
|
|
19
|
+
export declare const selectUploadProcessors: (ctx: UploadFileProcessorContext) => UploadFileProcessor[];
|
|
20
|
+
export declare const applyUploadProcessors: (data: Buffer, ctx: Omit<UploadFileProcessorContext, "sniff" | "totalSize">) => Promise<{
|
|
21
|
+
data: Buffer;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
applied: string[];
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { UploadFileProcessor } from './index';
|
|
2
|
+
export declare const looksLikeSvg: (sniff: Buffer) => boolean;
|
|
3
|
+
export declare const sanitizeSvg: (svg: string) => string;
|
|
4
|
+
export declare const sanitizeSvgProcessor: UploadFileProcessor;
|
|
5
|
+
//# sourceMappingURL=sanitizeSvg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitizeSvg.d.ts","sourceRoot":"","sources":["../../../../../src/uploads/api/file-uploads/processors/sanitizeSvg.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAA;AAgBlD,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,OAAmD,CAAA;AAEhG,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAGtC,CAAA;AAEJ,eAAO,MAAM,oBAAoB,EAAE,mBA0BlC,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,
|
|
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,3 +1,4 @@
|
|
|
1
1
|
import { ApiHandler } from '../../../../../../api/src';
|
|
2
|
-
|
|
2
|
+
import { SessionUser } from '../../file-uploads/shared';
|
|
3
|
+
export declare const getFile: ApiHandler<Record<string, never>, Record<string, never>, SessionUser>;
|
|
3
4
|
//# sourceMappingURL=getFile.d.ts.map
|
|
@@ -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;
|
|
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"}
|
package/dist/uploads.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const routes = Object.entries({
|
|
2
|
-
.../* @__PURE__ */ Object.assign({ "./api/file-uploads/handler.ts": () => import("./handler-
|
|
2
|
+
.../* @__PURE__ */ Object.assign({ "./api/file-uploads/handler.ts": () => import("./handler-BA2YiqnG.js"), "./api/files/handler.ts": () => import("./handler-Cohj3cz3.js") })
|
|
3
3
|
}).reduce((acc, [path, mod]) => {
|
|
4
4
|
acc[path.replace("./api/", "@rpcbase/server/uploads/api/")] = mod;
|
|
5
5
|
return acc;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpcbase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.481.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -85,13 +85,16 @@
|
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"connect-mongo": "6.0.0",
|
|
87
87
|
"connect-redis": "9.0.0",
|
|
88
|
+
"dompurify": "3.3.1",
|
|
88
89
|
"express-session": "1.18.2",
|
|
89
|
-
"mongodb": "7.0.0",
|
|
90
90
|
"http-proxy-3": "1.23.2",
|
|
91
|
+
"jsdom": "27.4.0",
|
|
92
|
+
"mongodb": "7.0.0",
|
|
91
93
|
"redis": "5.10.0",
|
|
92
94
|
"ws": "8.18.3"
|
|
93
95
|
},
|
|
94
96
|
"devDependencies": {
|
|
97
|
+
"@types/jsdom": "27.0.0",
|
|
95
98
|
"@types/ws": "8.18.1",
|
|
96
99
|
"request-ip": "3.3.0",
|
|
97
100
|
"resend": "6.6.0"
|