@momentumcms/server-analog 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/index.cjs +755 -212
- package/index.js +759 -210
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,3 +1,522 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// libs/storage/src/lib/storage.types.ts
|
|
12
|
+
var init_storage_types = __esm({
|
|
13
|
+
"libs/storage/src/lib/storage.types.ts"() {
|
|
14
|
+
"use strict";
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// libs/storage/src/lib/storage-local.ts
|
|
19
|
+
import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync, lstatSync } from "node:fs";
|
|
20
|
+
import { join, extname, resolve, normalize } from "node:path";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
function localStorageAdapter(options) {
|
|
23
|
+
const { directory, baseUrl } = options;
|
|
24
|
+
const resolvedRoot = resolve(directory);
|
|
25
|
+
if (!existsSync(resolvedRoot)) {
|
|
26
|
+
mkdirSync(resolvedRoot, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
function safePath(unsafePath) {
|
|
29
|
+
const normalized = normalize(unsafePath);
|
|
30
|
+
if (normalized.includes("..")) {
|
|
31
|
+
throw new Error("Invalid path: directory traversal not allowed");
|
|
32
|
+
}
|
|
33
|
+
const full = resolve(resolvedRoot, normalized);
|
|
34
|
+
if (!full.startsWith(resolvedRoot)) {
|
|
35
|
+
throw new Error("Invalid path: directory traversal not allowed");
|
|
36
|
+
}
|
|
37
|
+
if (existsSync(full) && lstatSync(full).isSymbolicLink()) {
|
|
38
|
+
throw new Error("Invalid path: symbolic links not allowed");
|
|
39
|
+
}
|
|
40
|
+
return full;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
async upload(file, uploadOptions) {
|
|
44
|
+
const ext = extname(file.originalName) || getExtensionFromMimeType(file.mimeType);
|
|
45
|
+
const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${randomUUID()}${ext}`;
|
|
46
|
+
const subdir = uploadOptions?.directory ?? "";
|
|
47
|
+
const targetDir = subdir ? safePath(subdir) : resolvedRoot;
|
|
48
|
+
if (subdir && !existsSync(targetDir)) {
|
|
49
|
+
mkdirSync(targetDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
const filePath = safePath(subdir ? join(subdir, filename) : filename);
|
|
52
|
+
const relativePath = subdir ? join(normalize(subdir), filename) : filename;
|
|
53
|
+
writeFileSync(filePath, file.buffer);
|
|
54
|
+
const url = baseUrl ? `${baseUrl}/${relativePath}` : `/api/media/file/${relativePath}`;
|
|
55
|
+
return {
|
|
56
|
+
path: relativePath,
|
|
57
|
+
url,
|
|
58
|
+
filename,
|
|
59
|
+
mimeType: file.mimeType,
|
|
60
|
+
size: file.size
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
async delete(path) {
|
|
64
|
+
const filePath = safePath(path);
|
|
65
|
+
if (existsSync(filePath)) {
|
|
66
|
+
unlinkSync(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
},
|
|
71
|
+
getUrl(path) {
|
|
72
|
+
return baseUrl ? `${baseUrl}/${path}` : `/api/media/file/${path}`;
|
|
73
|
+
},
|
|
74
|
+
async exists(path) {
|
|
75
|
+
const filePath = safePath(path);
|
|
76
|
+
return existsSync(filePath);
|
|
77
|
+
},
|
|
78
|
+
async read(path) {
|
|
79
|
+
const filePath = safePath(path);
|
|
80
|
+
if (existsSync(filePath)) {
|
|
81
|
+
return readFileSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function getExtensionFromMimeType(mimeType) {
|
|
88
|
+
const mimeToExt = {
|
|
89
|
+
"image/jpeg": ".jpg",
|
|
90
|
+
"image/png": ".png",
|
|
91
|
+
"image/gif": ".gif",
|
|
92
|
+
"image/webp": ".webp",
|
|
93
|
+
"image/svg+xml": ".svg",
|
|
94
|
+
"application/pdf": ".pdf",
|
|
95
|
+
"application/json": ".json",
|
|
96
|
+
"text/plain": ".txt",
|
|
97
|
+
"text/html": ".html",
|
|
98
|
+
"text/css": ".css",
|
|
99
|
+
"application/javascript": ".js",
|
|
100
|
+
"video/mp4": ".mp4",
|
|
101
|
+
"video/webm": ".webm",
|
|
102
|
+
"audio/mpeg": ".mp3",
|
|
103
|
+
"audio/wav": ".wav",
|
|
104
|
+
"application/zip": ".zip"
|
|
105
|
+
};
|
|
106
|
+
return mimeToExt[mimeType] ?? "";
|
|
107
|
+
}
|
|
108
|
+
var init_storage_local = __esm({
|
|
109
|
+
"libs/storage/src/lib/storage-local.ts"() {
|
|
110
|
+
"use strict";
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// libs/storage/src/lib/storage-s3.ts
|
|
115
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
116
|
+
import { extname as extname2 } from "node:path";
|
|
117
|
+
async function loadAwsSdk() {
|
|
118
|
+
if (S3Client)
|
|
119
|
+
return;
|
|
120
|
+
const s3Module = await import("@aws-sdk/client-s3");
|
|
121
|
+
const presignerModule = await import("@aws-sdk/s3-request-presigner");
|
|
122
|
+
S3Client = s3Module.S3Client;
|
|
123
|
+
PutObjectCommand = s3Module.PutObjectCommand;
|
|
124
|
+
DeleteObjectCommand = s3Module.DeleteObjectCommand;
|
|
125
|
+
HeadObjectCommand = s3Module.HeadObjectCommand;
|
|
126
|
+
GetObjectCommand = s3Module.GetObjectCommand;
|
|
127
|
+
getSignedUrl = presignerModule.getSignedUrl;
|
|
128
|
+
}
|
|
129
|
+
function s3StorageAdapter(options) {
|
|
130
|
+
const {
|
|
131
|
+
bucket,
|
|
132
|
+
region,
|
|
133
|
+
accessKeyId,
|
|
134
|
+
secretAccessKey,
|
|
135
|
+
endpoint,
|
|
136
|
+
baseUrl,
|
|
137
|
+
forcePathStyle = false,
|
|
138
|
+
acl = "private",
|
|
139
|
+
presignedUrlExpiry = 3600
|
|
140
|
+
} = options;
|
|
141
|
+
let client = null;
|
|
142
|
+
async function getClient() {
|
|
143
|
+
await loadAwsSdk();
|
|
144
|
+
if (!client) {
|
|
145
|
+
const clientConfig = {
|
|
146
|
+
region,
|
|
147
|
+
forcePathStyle
|
|
148
|
+
};
|
|
149
|
+
if (endpoint) {
|
|
150
|
+
clientConfig.endpoint = endpoint;
|
|
151
|
+
}
|
|
152
|
+
if (accessKeyId && secretAccessKey) {
|
|
153
|
+
clientConfig.credentials = {
|
|
154
|
+
accessKeyId,
|
|
155
|
+
secretAccessKey
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
client = new S3Client(clientConfig);
|
|
159
|
+
}
|
|
160
|
+
return client;
|
|
161
|
+
}
|
|
162
|
+
function getPublicUrl(key) {
|
|
163
|
+
if (baseUrl) {
|
|
164
|
+
return `${baseUrl}/${key}`;
|
|
165
|
+
}
|
|
166
|
+
if (endpoint) {
|
|
167
|
+
return `${endpoint}/${bucket}/${key}`;
|
|
168
|
+
}
|
|
169
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
async upload(file, uploadOptions) {
|
|
173
|
+
const s3 = await getClient();
|
|
174
|
+
const ext = extname2(file.originalName) || getExtensionFromMimeType2(file.mimeType);
|
|
175
|
+
const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${randomUUID2()}${ext}`;
|
|
176
|
+
const key = uploadOptions?.directory ? `${uploadOptions.directory}/${filename}` : filename;
|
|
177
|
+
await s3.send(
|
|
178
|
+
new PutObjectCommand({
|
|
179
|
+
Bucket: bucket,
|
|
180
|
+
Key: key,
|
|
181
|
+
Body: file.buffer,
|
|
182
|
+
ContentType: file.mimeType,
|
|
183
|
+
ACL: acl
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
path: key,
|
|
188
|
+
url: acl === "public-read" ? getPublicUrl(key) : `/api/media/file/${key}`,
|
|
189
|
+
filename,
|
|
190
|
+
mimeType: file.mimeType,
|
|
191
|
+
size: file.size
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
async delete(path) {
|
|
195
|
+
const s3 = await getClient();
|
|
196
|
+
try {
|
|
197
|
+
await s3.send(
|
|
198
|
+
new DeleteObjectCommand({
|
|
199
|
+
Bucket: bucket,
|
|
200
|
+
Key: path
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
return true;
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
getUrl(path) {
|
|
209
|
+
if (acl === "public-read") {
|
|
210
|
+
return getPublicUrl(path);
|
|
211
|
+
}
|
|
212
|
+
return `/api/media/file/${path}`;
|
|
213
|
+
},
|
|
214
|
+
async exists(path) {
|
|
215
|
+
const s3 = await getClient();
|
|
216
|
+
try {
|
|
217
|
+
await s3.send(
|
|
218
|
+
new HeadObjectCommand({
|
|
219
|
+
Bucket: bucket,
|
|
220
|
+
Key: path
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
return true;
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
async getSignedUrl(path, expiresIn) {
|
|
229
|
+
const s3 = await getClient();
|
|
230
|
+
await loadAwsSdk();
|
|
231
|
+
const command = new GetObjectCommand({
|
|
232
|
+
Bucket: bucket,
|
|
233
|
+
Key: path
|
|
234
|
+
});
|
|
235
|
+
return getSignedUrl(s3, command, {
|
|
236
|
+
expiresIn: expiresIn ?? presignedUrlExpiry
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
async read(path) {
|
|
240
|
+
const s3 = await getClient();
|
|
241
|
+
try {
|
|
242
|
+
const response = await s3.send(
|
|
243
|
+
new GetObjectCommand({
|
|
244
|
+
Bucket: bucket,
|
|
245
|
+
Key: path
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
if (!response.Body) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const chunks = [];
|
|
252
|
+
const body = response.Body;
|
|
253
|
+
for await (const chunk of body) {
|
|
254
|
+
chunks.push(chunk);
|
|
255
|
+
}
|
|
256
|
+
return Buffer.concat(chunks);
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function getExtensionFromMimeType2(mimeType) {
|
|
264
|
+
const mimeToExt = {
|
|
265
|
+
"image/jpeg": ".jpg",
|
|
266
|
+
"image/png": ".png",
|
|
267
|
+
"image/gif": ".gif",
|
|
268
|
+
"image/webp": ".webp",
|
|
269
|
+
"image/svg+xml": ".svg",
|
|
270
|
+
"application/pdf": ".pdf",
|
|
271
|
+
"application/json": ".json",
|
|
272
|
+
"text/plain": ".txt",
|
|
273
|
+
"text/html": ".html",
|
|
274
|
+
"text/css": ".css",
|
|
275
|
+
"application/javascript": ".js",
|
|
276
|
+
"video/mp4": ".mp4",
|
|
277
|
+
"video/webm": ".webm",
|
|
278
|
+
"audio/mpeg": ".mp3",
|
|
279
|
+
"audio/wav": ".wav",
|
|
280
|
+
"application/zip": ".zip"
|
|
281
|
+
};
|
|
282
|
+
return mimeToExt[mimeType] ?? "";
|
|
283
|
+
}
|
|
284
|
+
var S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, GetObjectCommand, getSignedUrl;
|
|
285
|
+
var init_storage_s3 = __esm({
|
|
286
|
+
"libs/storage/src/lib/storage-s3.ts"() {
|
|
287
|
+
"use strict";
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// libs/storage/src/lib/mime-validator.ts
|
|
292
|
+
function detectMimeType(buffer) {
|
|
293
|
+
for (const sig of FILE_SIGNATURES) {
|
|
294
|
+
const offset = sig.offset ?? 0;
|
|
295
|
+
if (buffer.length < offset + sig.bytes.length) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
let match = true;
|
|
299
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
300
|
+
if (buffer[offset + i] !== sig.bytes[i]) {
|
|
301
|
+
match = false;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (match) {
|
|
306
|
+
if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
|
|
307
|
+
if (buffer.length >= 12) {
|
|
308
|
+
const formatId = buffer.slice(8, 12).toString("ascii");
|
|
309
|
+
if (formatId === "WEBP") {
|
|
310
|
+
return "image/webp";
|
|
311
|
+
}
|
|
312
|
+
if (formatId === "WAVE") {
|
|
313
|
+
return "audio/wav";
|
|
314
|
+
}
|
|
315
|
+
if (formatId === "AVI ") {
|
|
316
|
+
return "video/avi";
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
|
|
321
|
+
const boxType = buffer.slice(4, 8).toString("ascii");
|
|
322
|
+
if (boxType === "ftyp") {
|
|
323
|
+
return "video/mp4";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return sig.mimeType;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (isTextContent(buffer)) {
|
|
330
|
+
const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
|
|
331
|
+
if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
|
|
332
|
+
return "application/json";
|
|
333
|
+
}
|
|
334
|
+
if (text2.trim().startsWith("<")) {
|
|
335
|
+
if (text2.includes("<svg")) {
|
|
336
|
+
return "image/svg+xml";
|
|
337
|
+
}
|
|
338
|
+
if (text2.includes("<!DOCTYPE html") || text2.includes("<html")) {
|
|
339
|
+
return "text/html";
|
|
340
|
+
}
|
|
341
|
+
return "application/xml";
|
|
342
|
+
}
|
|
343
|
+
return "text/plain";
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
function isTextContent(buffer) {
|
|
348
|
+
const checkLength = Math.min(buffer.length, 512);
|
|
349
|
+
for (let i = 0; i < checkLength; i++) {
|
|
350
|
+
const byte = buffer[i];
|
|
351
|
+
if (byte < 9 || // Control chars before tab
|
|
352
|
+
byte > 13 && byte < 32 || // Control chars between CR and space
|
|
353
|
+
byte === 127) {
|
|
354
|
+
if (byte >= 128 && byte <= 191) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (byte >= 192 && byte <= 247) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
function mimeTypeMatches(mimeType, pattern) {
|
|
366
|
+
if (pattern === "*" || pattern === "*/*") {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
if (pattern.endsWith("/*")) {
|
|
370
|
+
const category = pattern.slice(0, -2);
|
|
371
|
+
return mimeType.startsWith(`${category}/`);
|
|
372
|
+
}
|
|
373
|
+
return mimeType === pattern;
|
|
374
|
+
}
|
|
375
|
+
function isMimeTypeAllowed(mimeType, allowedTypes) {
|
|
376
|
+
if (allowedTypes.length === 0) {
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
return allowedTypes.some((pattern) => mimeTypeMatches(mimeType, pattern));
|
|
380
|
+
}
|
|
381
|
+
function validateMimeType(buffer, claimedType, allowedTypes) {
|
|
382
|
+
const detectedType = detectMimeType(buffer);
|
|
383
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
384
|
+
const typeToCheck = detectedType ?? claimedType;
|
|
385
|
+
if (!isMimeTypeAllowed(typeToCheck, allowedTypes)) {
|
|
386
|
+
return {
|
|
387
|
+
valid: false,
|
|
388
|
+
detectedType,
|
|
389
|
+
claimedType,
|
|
390
|
+
error: `File type '${typeToCheck}' is not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (!detectedType) {
|
|
395
|
+
return {
|
|
396
|
+
valid: true,
|
|
397
|
+
detectedType: null,
|
|
398
|
+
claimedType
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const compatible = areMimeTypesCompatible(detectedType, claimedType);
|
|
402
|
+
if (!compatible) {
|
|
403
|
+
return {
|
|
404
|
+
valid: false,
|
|
405
|
+
detectedType,
|
|
406
|
+
claimedType,
|
|
407
|
+
error: `File appears to be '${detectedType}' but was uploaded as '${claimedType}'`
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
valid: true,
|
|
412
|
+
detectedType,
|
|
413
|
+
claimedType
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function areMimeTypesCompatible(detected, claimed) {
|
|
417
|
+
if (detected === claimed) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
const [detectedCategory] = detected.split("/");
|
|
421
|
+
const [claimedCategory] = claimed.split("/");
|
|
422
|
+
if (detectedCategory !== claimedCategory) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
const variations = {
|
|
426
|
+
"image/jpeg": ["image/jpg", "image/pjpeg"],
|
|
427
|
+
"text/plain": ["text/x-plain"],
|
|
428
|
+
"application/json": ["text/json"],
|
|
429
|
+
"application/javascript": ["text/javascript", "application/x-javascript"]
|
|
430
|
+
};
|
|
431
|
+
const allowedVariations = variations[detected];
|
|
432
|
+
if (allowedVariations && allowedVariations.includes(claimed)) {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
for (const [canonical, variants] of Object.entries(variations)) {
|
|
436
|
+
if (variants.includes(detected) && (canonical === claimed || variants.includes(claimed))) {
|
|
437
|
+
return true;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
var FILE_SIGNATURES;
|
|
443
|
+
var init_mime_validator = __esm({
|
|
444
|
+
"libs/storage/src/lib/mime-validator.ts"() {
|
|
445
|
+
"use strict";
|
|
446
|
+
FILE_SIGNATURES = [
|
|
447
|
+
// Images
|
|
448
|
+
{ mimeType: "image/jpeg", bytes: [255, 216, 255] },
|
|
449
|
+
{ mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
|
|
450
|
+
{ mimeType: "image/gif", bytes: [71, 73, 70, 56] },
|
|
451
|
+
// GIF8
|
|
452
|
+
{ mimeType: "image/webp", bytes: [82, 73, 70, 70], offset: 0 },
|
|
453
|
+
// RIFF (need to check for WEBP at offset 8)
|
|
454
|
+
{ mimeType: "image/bmp", bytes: [66, 77] },
|
|
455
|
+
// BM
|
|
456
|
+
{ mimeType: "image/tiff", bytes: [73, 73, 42, 0] },
|
|
457
|
+
// Little-endian TIFF
|
|
458
|
+
{ mimeType: "image/tiff", bytes: [77, 77, 0, 42] },
|
|
459
|
+
// Big-endian TIFF
|
|
460
|
+
{ mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
|
|
461
|
+
// ICO
|
|
462
|
+
{ mimeType: "image/svg+xml", bytes: [60, 115, 118, 103] },
|
|
463
|
+
// <svg (partial match)
|
|
464
|
+
// Documents
|
|
465
|
+
{ mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
|
|
466
|
+
// %PDF
|
|
467
|
+
// Archives
|
|
468
|
+
{ mimeType: "application/zip", bytes: [80, 75, 3, 4] },
|
|
469
|
+
// PK
|
|
470
|
+
{ mimeType: "application/gzip", bytes: [31, 139] },
|
|
471
|
+
{ mimeType: "application/x-rar-compressed", bytes: [82, 97, 114, 33] },
|
|
472
|
+
// Rar!
|
|
473
|
+
// Audio
|
|
474
|
+
{ mimeType: "audio/mpeg", bytes: [73, 68, 51] },
|
|
475
|
+
// ID3 (MP3)
|
|
476
|
+
{ mimeType: "audio/mpeg", bytes: [255, 251] },
|
|
477
|
+
// MP3 frame sync
|
|
478
|
+
{ mimeType: "audio/wav", bytes: [82, 73, 70, 70] },
|
|
479
|
+
// RIFF (need to check for WAVE)
|
|
480
|
+
{ mimeType: "audio/ogg", bytes: [79, 103, 103, 83] },
|
|
481
|
+
// OggS
|
|
482
|
+
{ mimeType: "audio/flac", bytes: [102, 76, 97, 67] },
|
|
483
|
+
// fLaC
|
|
484
|
+
// Video
|
|
485
|
+
{ mimeType: "video/mp4", bytes: [0, 0, 0], offset: 0 },
|
|
486
|
+
// Need to check for ftyp at offset 4
|
|
487
|
+
{ mimeType: "video/webm", bytes: [26, 69, 223, 163] },
|
|
488
|
+
// EBML header
|
|
489
|
+
{ mimeType: "video/avi", bytes: [82, 73, 70, 70] },
|
|
490
|
+
// RIFF (need to check for AVI)
|
|
491
|
+
// Executables (for blocking)
|
|
492
|
+
{ mimeType: "application/x-executable", bytes: [127, 69, 76, 70] },
|
|
493
|
+
// ELF
|
|
494
|
+
{ mimeType: "application/x-msdownload", bytes: [77, 90] }
|
|
495
|
+
// MZ (Windows EXE)
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// libs/storage/src/index.ts
|
|
501
|
+
var src_exports = {};
|
|
502
|
+
__export(src_exports, {
|
|
503
|
+
detectMimeType: () => detectMimeType,
|
|
504
|
+
isMimeTypeAllowed: () => isMimeTypeAllowed,
|
|
505
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
506
|
+
mimeTypeMatches: () => mimeTypeMatches,
|
|
507
|
+
s3StorageAdapter: () => s3StorageAdapter,
|
|
508
|
+
validateMimeType: () => validateMimeType
|
|
509
|
+
});
|
|
510
|
+
var init_src = __esm({
|
|
511
|
+
"libs/storage/src/index.ts"() {
|
|
512
|
+
"use strict";
|
|
513
|
+
init_storage_types();
|
|
514
|
+
init_storage_local();
|
|
515
|
+
init_storage_s3();
|
|
516
|
+
init_mime_validator();
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
1
520
|
// libs/logger/src/lib/log-level.ts
|
|
2
521
|
var LOG_LEVEL_VALUES = {
|
|
3
522
|
debug: 0,
|
|
@@ -253,6 +772,9 @@ function getSoftDeleteField(config) {
|
|
|
253
772
|
const sdConfig = config.softDelete;
|
|
254
773
|
return sdConfig.field ?? "deletedAt";
|
|
255
774
|
}
|
|
775
|
+
function isUploadCollection(config) {
|
|
776
|
+
return config.upload != null;
|
|
777
|
+
}
|
|
256
778
|
|
|
257
779
|
// libs/core/src/lib/fields/field.types.ts
|
|
258
780
|
var ReferentialIntegrityError = class extends Error {
|
|
@@ -2945,210 +3467,8 @@ function createAdapterApiKeyStore(adapter) {
|
|
|
2945
3467
|
};
|
|
2946
3468
|
}
|
|
2947
3469
|
|
|
2948
|
-
// libs/storage/src/lib/mime-validator.ts
|
|
2949
|
-
var FILE_SIGNATURES = [
|
|
2950
|
-
// Images
|
|
2951
|
-
{ mimeType: "image/jpeg", bytes: [255, 216, 255] },
|
|
2952
|
-
{ mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
|
|
2953
|
-
{ mimeType: "image/gif", bytes: [71, 73, 70, 56] },
|
|
2954
|
-
// GIF8
|
|
2955
|
-
{ mimeType: "image/webp", bytes: [82, 73, 70, 70], offset: 0 },
|
|
2956
|
-
// RIFF (need to check for WEBP at offset 8)
|
|
2957
|
-
{ mimeType: "image/bmp", bytes: [66, 77] },
|
|
2958
|
-
// BM
|
|
2959
|
-
{ mimeType: "image/tiff", bytes: [73, 73, 42, 0] },
|
|
2960
|
-
// Little-endian TIFF
|
|
2961
|
-
{ mimeType: "image/tiff", bytes: [77, 77, 0, 42] },
|
|
2962
|
-
// Big-endian TIFF
|
|
2963
|
-
{ mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
|
|
2964
|
-
// ICO
|
|
2965
|
-
{ mimeType: "image/svg+xml", bytes: [60, 115, 118, 103] },
|
|
2966
|
-
// <svg (partial match)
|
|
2967
|
-
// Documents
|
|
2968
|
-
{ mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
|
|
2969
|
-
// %PDF
|
|
2970
|
-
// Archives
|
|
2971
|
-
{ mimeType: "application/zip", bytes: [80, 75, 3, 4] },
|
|
2972
|
-
// PK
|
|
2973
|
-
{ mimeType: "application/gzip", bytes: [31, 139] },
|
|
2974
|
-
{ mimeType: "application/x-rar-compressed", bytes: [82, 97, 114, 33] },
|
|
2975
|
-
// Rar!
|
|
2976
|
-
// Audio
|
|
2977
|
-
{ mimeType: "audio/mpeg", bytes: [73, 68, 51] },
|
|
2978
|
-
// ID3 (MP3)
|
|
2979
|
-
{ mimeType: "audio/mpeg", bytes: [255, 251] },
|
|
2980
|
-
// MP3 frame sync
|
|
2981
|
-
{ mimeType: "audio/wav", bytes: [82, 73, 70, 70] },
|
|
2982
|
-
// RIFF (need to check for WAVE)
|
|
2983
|
-
{ mimeType: "audio/ogg", bytes: [79, 103, 103, 83] },
|
|
2984
|
-
// OggS
|
|
2985
|
-
{ mimeType: "audio/flac", bytes: [102, 76, 97, 67] },
|
|
2986
|
-
// fLaC
|
|
2987
|
-
// Video
|
|
2988
|
-
{ mimeType: "video/mp4", bytes: [0, 0, 0], offset: 0 },
|
|
2989
|
-
// Need to check for ftyp at offset 4
|
|
2990
|
-
{ mimeType: "video/webm", bytes: [26, 69, 223, 163] },
|
|
2991
|
-
// EBML header
|
|
2992
|
-
{ mimeType: "video/avi", bytes: [82, 73, 70, 70] },
|
|
2993
|
-
// RIFF (need to check for AVI)
|
|
2994
|
-
// Executables (for blocking)
|
|
2995
|
-
{ mimeType: "application/x-executable", bytes: [127, 69, 76, 70] },
|
|
2996
|
-
// ELF
|
|
2997
|
-
{ mimeType: "application/x-msdownload", bytes: [77, 90] }
|
|
2998
|
-
// MZ (Windows EXE)
|
|
2999
|
-
];
|
|
3000
|
-
function detectMimeType(buffer) {
|
|
3001
|
-
for (const sig of FILE_SIGNATURES) {
|
|
3002
|
-
const offset = sig.offset ?? 0;
|
|
3003
|
-
if (buffer.length < offset + sig.bytes.length) {
|
|
3004
|
-
continue;
|
|
3005
|
-
}
|
|
3006
|
-
let match = true;
|
|
3007
|
-
for (let i = 0; i < sig.bytes.length; i++) {
|
|
3008
|
-
if (buffer[offset + i] !== sig.bytes[i]) {
|
|
3009
|
-
match = false;
|
|
3010
|
-
break;
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
if (match) {
|
|
3014
|
-
if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
|
|
3015
|
-
if (buffer.length >= 12) {
|
|
3016
|
-
const formatId = buffer.slice(8, 12).toString("ascii");
|
|
3017
|
-
if (formatId === "WEBP") {
|
|
3018
|
-
return "image/webp";
|
|
3019
|
-
}
|
|
3020
|
-
if (formatId === "WAVE") {
|
|
3021
|
-
return "audio/wav";
|
|
3022
|
-
}
|
|
3023
|
-
if (formatId === "AVI ") {
|
|
3024
|
-
return "video/avi";
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
|
|
3029
|
-
const boxType = buffer.slice(4, 8).toString("ascii");
|
|
3030
|
-
if (boxType === "ftyp") {
|
|
3031
|
-
return "video/mp4";
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return sig.mimeType;
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
if (isTextContent(buffer)) {
|
|
3038
|
-
const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
|
|
3039
|
-
if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
|
|
3040
|
-
return "application/json";
|
|
3041
|
-
}
|
|
3042
|
-
if (text2.trim().startsWith("<")) {
|
|
3043
|
-
if (text2.includes("<svg")) {
|
|
3044
|
-
return "image/svg+xml";
|
|
3045
|
-
}
|
|
3046
|
-
if (text2.includes("<!DOCTYPE html") || text2.includes("<html")) {
|
|
3047
|
-
return "text/html";
|
|
3048
|
-
}
|
|
3049
|
-
return "application/xml";
|
|
3050
|
-
}
|
|
3051
|
-
return "text/plain";
|
|
3052
|
-
}
|
|
3053
|
-
return null;
|
|
3054
|
-
}
|
|
3055
|
-
function isTextContent(buffer) {
|
|
3056
|
-
const checkLength = Math.min(buffer.length, 512);
|
|
3057
|
-
for (let i = 0; i < checkLength; i++) {
|
|
3058
|
-
const byte = buffer[i];
|
|
3059
|
-
if (byte < 9 || // Control chars before tab
|
|
3060
|
-
byte > 13 && byte < 32 || // Control chars between CR and space
|
|
3061
|
-
byte === 127) {
|
|
3062
|
-
if (byte >= 128 && byte <= 191) {
|
|
3063
|
-
continue;
|
|
3064
|
-
}
|
|
3065
|
-
if (byte >= 192 && byte <= 247) {
|
|
3066
|
-
continue;
|
|
3067
|
-
}
|
|
3068
|
-
return false;
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
return true;
|
|
3072
|
-
}
|
|
3073
|
-
function mimeTypeMatches(mimeType, pattern) {
|
|
3074
|
-
if (pattern === "*" || pattern === "*/*") {
|
|
3075
|
-
return true;
|
|
3076
|
-
}
|
|
3077
|
-
if (pattern.endsWith("/*")) {
|
|
3078
|
-
const category = pattern.slice(0, -2);
|
|
3079
|
-
return mimeType.startsWith(`${category}/`);
|
|
3080
|
-
}
|
|
3081
|
-
return mimeType === pattern;
|
|
3082
|
-
}
|
|
3083
|
-
function isMimeTypeAllowed(mimeType, allowedTypes) {
|
|
3084
|
-
if (allowedTypes.length === 0) {
|
|
3085
|
-
return true;
|
|
3086
|
-
}
|
|
3087
|
-
return allowedTypes.some((pattern) => mimeTypeMatches(mimeType, pattern));
|
|
3088
|
-
}
|
|
3089
|
-
function validateMimeType(buffer, claimedType, allowedTypes) {
|
|
3090
|
-
const detectedType = detectMimeType(buffer);
|
|
3091
|
-
if (allowedTypes && allowedTypes.length > 0) {
|
|
3092
|
-
const typeToCheck = detectedType ?? claimedType;
|
|
3093
|
-
if (!isMimeTypeAllowed(typeToCheck, allowedTypes)) {
|
|
3094
|
-
return {
|
|
3095
|
-
valid: false,
|
|
3096
|
-
detectedType,
|
|
3097
|
-
claimedType,
|
|
3098
|
-
error: `File type '${typeToCheck}' is not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
3099
|
-
};
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
if (!detectedType) {
|
|
3103
|
-
return {
|
|
3104
|
-
valid: true,
|
|
3105
|
-
detectedType: null,
|
|
3106
|
-
claimedType
|
|
3107
|
-
};
|
|
3108
|
-
}
|
|
3109
|
-
const compatible = areMimeTypesCompatible(detectedType, claimedType);
|
|
3110
|
-
if (!compatible) {
|
|
3111
|
-
return {
|
|
3112
|
-
valid: false,
|
|
3113
|
-
detectedType,
|
|
3114
|
-
claimedType,
|
|
3115
|
-
error: `File appears to be '${detectedType}' but was uploaded as '${claimedType}'`
|
|
3116
|
-
};
|
|
3117
|
-
}
|
|
3118
|
-
return {
|
|
3119
|
-
valid: true,
|
|
3120
|
-
detectedType,
|
|
3121
|
-
claimedType
|
|
3122
|
-
};
|
|
3123
|
-
}
|
|
3124
|
-
function areMimeTypesCompatible(detected, claimed) {
|
|
3125
|
-
if (detected === claimed) {
|
|
3126
|
-
return true;
|
|
3127
|
-
}
|
|
3128
|
-
const [detectedCategory] = detected.split("/");
|
|
3129
|
-
const [claimedCategory] = claimed.split("/");
|
|
3130
|
-
if (detectedCategory !== claimedCategory) {
|
|
3131
|
-
return false;
|
|
3132
|
-
}
|
|
3133
|
-
const variations = {
|
|
3134
|
-
"image/jpeg": ["image/jpg", "image/pjpeg"],
|
|
3135
|
-
"text/plain": ["text/x-plain"],
|
|
3136
|
-
"application/json": ["text/json"],
|
|
3137
|
-
"application/javascript": ["text/javascript", "application/x-javascript"]
|
|
3138
|
-
};
|
|
3139
|
-
const allowedVariations = variations[detected];
|
|
3140
|
-
if (allowedVariations && allowedVariations.includes(claimed)) {
|
|
3141
|
-
return true;
|
|
3142
|
-
}
|
|
3143
|
-
for (const [canonical, variants] of Object.entries(variations)) {
|
|
3144
|
-
if (variants.includes(detected) && (canonical === claimed || variants.includes(claimed))) {
|
|
3145
|
-
return true;
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
return false;
|
|
3149
|
-
}
|
|
3150
|
-
|
|
3151
3470
|
// libs/server-core/src/lib/upload-handler.ts
|
|
3471
|
+
init_src();
|
|
3152
3472
|
function getUploadConfig(config) {
|
|
3153
3473
|
if (!config.storage?.adapter) {
|
|
3154
3474
|
return null;
|
|
@@ -3263,6 +3583,64 @@ async function handleUpload(config, request) {
|
|
|
3263
3583
|
};
|
|
3264
3584
|
}
|
|
3265
3585
|
}
|
|
3586
|
+
async function handleCollectionUpload(globalConfig, request) {
|
|
3587
|
+
const { adapter } = globalConfig;
|
|
3588
|
+
const { file, user, fields, collectionSlug, collectionUpload } = request;
|
|
3589
|
+
const maxFileSize = collectionUpload.maxFileSize ?? globalConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
3590
|
+
const allowedMimeTypes = collectionUpload.mimeTypes ?? globalConfig.allowedMimeTypes ?? [];
|
|
3591
|
+
try {
|
|
3592
|
+
if (!user) {
|
|
3593
|
+
return {
|
|
3594
|
+
status: 401,
|
|
3595
|
+
error: "Authentication required to upload files"
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
const sizeError = validateFileSize(file, maxFileSize);
|
|
3599
|
+
if (sizeError) {
|
|
3600
|
+
return { status: 400, error: sizeError };
|
|
3601
|
+
}
|
|
3602
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
3603
|
+
if (mimeError) {
|
|
3604
|
+
return { status: 400, error: mimeError };
|
|
3605
|
+
}
|
|
3606
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
3607
|
+
const magicByteResult = validateMimeType(
|
|
3608
|
+
file.buffer,
|
|
3609
|
+
file.mimeType,
|
|
3610
|
+
allowedMimeTypes
|
|
3611
|
+
);
|
|
3612
|
+
if (!magicByteResult.valid) {
|
|
3613
|
+
return {
|
|
3614
|
+
status: 400,
|
|
3615
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
const storedFile = await adapter.upload(file);
|
|
3620
|
+
const docData = {
|
|
3621
|
+
...fields,
|
|
3622
|
+
filename: file.originalName,
|
|
3623
|
+
mimeType: file.mimeType,
|
|
3624
|
+
filesize: file.size,
|
|
3625
|
+
path: storedFile.path,
|
|
3626
|
+
url: storedFile.url
|
|
3627
|
+
};
|
|
3628
|
+
const api = getMomentumAPI().setContext({ user });
|
|
3629
|
+
const doc = await api.collection(collectionSlug).create(docData);
|
|
3630
|
+
return {
|
|
3631
|
+
status: 201,
|
|
3632
|
+
doc
|
|
3633
|
+
};
|
|
3634
|
+
} catch (error) {
|
|
3635
|
+
if (error instanceof Error) {
|
|
3636
|
+
if (error.message.includes("Access denied")) {
|
|
3637
|
+
return { status: 403, error: error.message };
|
|
3638
|
+
}
|
|
3639
|
+
return { status: 500, error: `Upload failed: ${error.message}` };
|
|
3640
|
+
}
|
|
3641
|
+
return { status: 500, error: "Upload failed: Unknown error" };
|
|
3642
|
+
}
|
|
3643
|
+
}
|
|
3266
3644
|
async function handleFileGet(adapter, path) {
|
|
3267
3645
|
if (!adapter.read) {
|
|
3268
3646
|
return null;
|
|
@@ -4345,6 +4723,39 @@ function coerceCsvValue(value, fieldType) {
|
|
|
4345
4723
|
}
|
|
4346
4724
|
|
|
4347
4725
|
// libs/server-analog/src/lib/server-analog.ts
|
|
4726
|
+
function nestBracketParams(flat) {
|
|
4727
|
+
const result = {};
|
|
4728
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
4729
|
+
const bracketIdx = key.indexOf("[");
|
|
4730
|
+
if (bracketIdx === -1) {
|
|
4731
|
+
result[key] = value;
|
|
4732
|
+
continue;
|
|
4733
|
+
}
|
|
4734
|
+
const rootKey = key.slice(0, bracketIdx);
|
|
4735
|
+
const bracketPart = key.slice(bracketIdx);
|
|
4736
|
+
const parts = [];
|
|
4737
|
+
const bracketRegex = /\[([^\]]*)\]/g;
|
|
4738
|
+
let m;
|
|
4739
|
+
while ((m = bracketRegex.exec(bracketPart)) !== null) {
|
|
4740
|
+
parts.push(m[1]);
|
|
4741
|
+
}
|
|
4742
|
+
if (parts.length === 0) {
|
|
4743
|
+
result[key] = value;
|
|
4744
|
+
continue;
|
|
4745
|
+
}
|
|
4746
|
+
let current = result[rootKey] ?? {};
|
|
4747
|
+
result[rootKey] = current;
|
|
4748
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
4749
|
+
const part = parts[i];
|
|
4750
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
4751
|
+
current[part] = {};
|
|
4752
|
+
}
|
|
4753
|
+
current = current[part];
|
|
4754
|
+
}
|
|
4755
|
+
current[parts[parts.length - 1]] = value;
|
|
4756
|
+
}
|
|
4757
|
+
return result;
|
|
4758
|
+
}
|
|
4348
4759
|
function toMomentumMethod(m) {
|
|
4349
4760
|
if (m === "GET" || m === "POST" || m === "PATCH" || m === "PUT" || m === "DELETE") {
|
|
4350
4761
|
return m;
|
|
@@ -4360,7 +4771,7 @@ function createMomentumHandler(config) {
|
|
|
4360
4771
|
const pathSegments = (params["momentum"] ?? "").split("/").filter(Boolean);
|
|
4361
4772
|
const collectionSlug = pathSegments[0] ?? "";
|
|
4362
4773
|
const id = pathSegments[1];
|
|
4363
|
-
const queryParams = getQuery(event);
|
|
4774
|
+
const queryParams = nestBracketParams(getQuery(event));
|
|
4364
4775
|
const sortParam = queryParams["sort"];
|
|
4365
4776
|
const query = {
|
|
4366
4777
|
limit: queryParams["limit"] ? Number(queryParams["limit"]) : void 0,
|
|
@@ -4493,7 +4904,7 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4493
4904
|
const user = context?.user;
|
|
4494
4905
|
const params = utils.getRouterParams(event);
|
|
4495
4906
|
const pathSegments = (params["momentum"] ?? "").split("/").filter(Boolean);
|
|
4496
|
-
const queryParams = utils.getQuery(event);
|
|
4907
|
+
const queryParams = nestBracketParams(utils.getQuery(event));
|
|
4497
4908
|
const seg0 = pathSegments[0] ?? "";
|
|
4498
4909
|
const seg1 = pathSegments[1];
|
|
4499
4910
|
const seg2 = pathSegments[2];
|
|
@@ -4736,7 +5147,7 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4736
5147
|
utils.setResponseStatus(event, 400);
|
|
4737
5148
|
return { error: "File path required" };
|
|
4738
5149
|
}
|
|
4739
|
-
const { normalize, isAbsolute, resolve, sep } = await import("node:path");
|
|
5150
|
+
const { normalize: normalize2, isAbsolute, resolve: resolve2, sep } = await import("node:path");
|
|
4740
5151
|
let decodedPath;
|
|
4741
5152
|
try {
|
|
4742
5153
|
decodedPath = decodeURIComponent(rawPath);
|
|
@@ -4744,13 +5155,17 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4744
5155
|
utils.setResponseStatus(event, 400);
|
|
4745
5156
|
return { error: "Invalid path encoding" };
|
|
4746
5157
|
}
|
|
4747
|
-
|
|
4748
|
-
if (isAbsolute(filePath) || filePath.includes("..") || filePath.includes(`${sep}..`)) {
|
|
5158
|
+
if (decodedPath.includes("..")) {
|
|
4749
5159
|
utils.setResponseStatus(event, 403);
|
|
4750
5160
|
return { error: "Invalid file path" };
|
|
4751
5161
|
}
|
|
4752
|
-
const
|
|
4753
|
-
|
|
5162
|
+
const filePath = normalize2(decodedPath);
|
|
5163
|
+
if (isAbsolute(filePath)) {
|
|
5164
|
+
utils.setResponseStatus(event, 403);
|
|
5165
|
+
return { error: "Invalid file path" };
|
|
5166
|
+
}
|
|
5167
|
+
const fakeRoot = resolve2("/safe-root");
|
|
5168
|
+
const resolved = resolve2(fakeRoot, filePath);
|
|
4754
5169
|
if (!resolved.startsWith(fakeRoot + sep) && resolved !== fakeRoot) {
|
|
4755
5170
|
utils.setResponseStatus(event, 403);
|
|
4756
5171
|
return { error: "Invalid file path" };
|
|
@@ -5247,6 +5662,140 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5247
5662
|
return { error: sanitizeErrorMessage(error, "Import failed") };
|
|
5248
5663
|
}
|
|
5249
5664
|
}
|
|
5665
|
+
const postUploadCol = seg0 ? config.collections.find((c) => c.slug === seg0) : void 0;
|
|
5666
|
+
if (method === "POST" && seg0 && !seg1 && postUploadCol && isUploadCollection(postUploadCol)) {
|
|
5667
|
+
if (!user) {
|
|
5668
|
+
utils.setResponseStatus(event, 401);
|
|
5669
|
+
return { error: "Authentication required to upload files" };
|
|
5670
|
+
}
|
|
5671
|
+
const uploadConfig = getUploadConfig(config);
|
|
5672
|
+
if (!uploadConfig) {
|
|
5673
|
+
utils.setResponseStatus(event, 500);
|
|
5674
|
+
return { error: "Storage not configured" };
|
|
5675
|
+
}
|
|
5676
|
+
const formData = await utils.readMultipartFormData(event);
|
|
5677
|
+
if (!formData || formData.length === 0) {
|
|
5678
|
+
utils.setResponseStatus(event, 400);
|
|
5679
|
+
return { error: "No file provided" };
|
|
5680
|
+
}
|
|
5681
|
+
const fileField = formData.find((f) => f.name === "file");
|
|
5682
|
+
if (!fileField || !fileField.filename) {
|
|
5683
|
+
utils.setResponseStatus(event, 400);
|
|
5684
|
+
return { error: "No file provided" };
|
|
5685
|
+
}
|
|
5686
|
+
const file = {
|
|
5687
|
+
originalName: fileField.filename,
|
|
5688
|
+
mimeType: fileField.type ?? "application/octet-stream",
|
|
5689
|
+
size: fileField.data.length,
|
|
5690
|
+
buffer: fileField.data
|
|
5691
|
+
};
|
|
5692
|
+
const fields = {};
|
|
5693
|
+
for (const field of formData) {
|
|
5694
|
+
if (field.name !== "file" && field.name) {
|
|
5695
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
const uploadRequest = {
|
|
5699
|
+
file,
|
|
5700
|
+
user,
|
|
5701
|
+
fields,
|
|
5702
|
+
collectionSlug: seg0,
|
|
5703
|
+
collectionUpload: postUploadCol.upload
|
|
5704
|
+
};
|
|
5705
|
+
const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
|
|
5706
|
+
utils.setResponseStatus(event, response2.status);
|
|
5707
|
+
return response2;
|
|
5708
|
+
}
|
|
5709
|
+
const patchUploadCol = seg0 ? config.collections.find((c) => c.slug === seg0) : void 0;
|
|
5710
|
+
if (method === "PATCH" && seg0 && seg1 && patchUploadCol && isUploadCollection(patchUploadCol)) {
|
|
5711
|
+
if (!user) {
|
|
5712
|
+
utils.setResponseStatus(event, 401);
|
|
5713
|
+
return { error: "Authentication required to upload files" };
|
|
5714
|
+
}
|
|
5715
|
+
const formData = await utils.readMultipartFormData(event);
|
|
5716
|
+
if (formData) {
|
|
5717
|
+
const uploadConfig = getUploadConfig(config);
|
|
5718
|
+
if (!uploadConfig) {
|
|
5719
|
+
utils.setResponseStatus(event, 500);
|
|
5720
|
+
return { error: "Storage not configured" };
|
|
5721
|
+
}
|
|
5722
|
+
const fileField = formData.find((f) => f.name === "file");
|
|
5723
|
+
if (fileField?.filename) {
|
|
5724
|
+
const file = {
|
|
5725
|
+
originalName: fileField.filename,
|
|
5726
|
+
mimeType: fileField.type ?? "application/octet-stream",
|
|
5727
|
+
size: fileField.data.length,
|
|
5728
|
+
buffer: fileField.data
|
|
5729
|
+
};
|
|
5730
|
+
const maxFileSize = patchUploadCol.upload?.maxFileSize ?? uploadConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
5731
|
+
const allowedMimeTypes = patchUploadCol.upload?.mimeTypes ?? uploadConfig.allowedMimeTypes ?? [];
|
|
5732
|
+
if (file.size > maxFileSize) {
|
|
5733
|
+
const maxMB = (maxFileSize / (1024 * 1024)).toFixed(1);
|
|
5734
|
+
utils.setResponseStatus(event, 400);
|
|
5735
|
+
return { error: `File too large. Maximum size is ${maxMB}MB` };
|
|
5736
|
+
}
|
|
5737
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
5738
|
+
if (mimeError) {
|
|
5739
|
+
utils.setResponseStatus(event, 400);
|
|
5740
|
+
return { error: mimeError };
|
|
5741
|
+
}
|
|
5742
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
5743
|
+
const { validateMimeType: validateMimeByMagicBytes } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
5744
|
+
const magicByteResult = validateMimeByMagicBytes(
|
|
5745
|
+
file.buffer,
|
|
5746
|
+
file.mimeType,
|
|
5747
|
+
allowedMimeTypes
|
|
5748
|
+
);
|
|
5749
|
+
if (!magicByteResult.valid) {
|
|
5750
|
+
utils.setResponseStatus(event, 400);
|
|
5751
|
+
return {
|
|
5752
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
5753
|
+
};
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
const storedFile = await uploadConfig.adapter.upload(file);
|
|
5757
|
+
const fields = {};
|
|
5758
|
+
for (const field of formData ?? []) {
|
|
5759
|
+
if (field.name !== "file" && field.name) {
|
|
5760
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
const updateData = {
|
|
5764
|
+
...fields,
|
|
5765
|
+
filename: file.originalName,
|
|
5766
|
+
mimeType: file.mimeType,
|
|
5767
|
+
filesize: file.size,
|
|
5768
|
+
path: storedFile.path,
|
|
5769
|
+
url: storedFile.url
|
|
5770
|
+
};
|
|
5771
|
+
try {
|
|
5772
|
+
const api = getMomentumAPI().setContext({ user });
|
|
5773
|
+
const doc = await api.collection(seg0).update(seg1, updateData);
|
|
5774
|
+
return { doc };
|
|
5775
|
+
} catch (error) {
|
|
5776
|
+
utils.setResponseStatus(event, 500);
|
|
5777
|
+
return { error: sanitizeErrorMessage(error, "Failed to update document") };
|
|
5778
|
+
}
|
|
5779
|
+
} else {
|
|
5780
|
+
const fields = {};
|
|
5781
|
+
for (const field of formData ?? []) {
|
|
5782
|
+
if (field.name) {
|
|
5783
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
const request2 = {
|
|
5787
|
+
method: "PATCH",
|
|
5788
|
+
collectionSlug: seg0,
|
|
5789
|
+
id: seg1,
|
|
5790
|
+
body: fields,
|
|
5791
|
+
user
|
|
5792
|
+
};
|
|
5793
|
+
const response2 = await handlers.routeRequest(request2);
|
|
5794
|
+
utils.setResponseStatus(event, response2.status ?? 200);
|
|
5795
|
+
return response2;
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5250
5799
|
const collectionSlug = seg0;
|
|
5251
5800
|
const id = seg1;
|
|
5252
5801
|
const sortParam = queryParams["sort"];
|