@momentumcms/server-analog 0.3.0 → 0.4.1
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.cjs
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,14 +30,524 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
30
|
-
// libs/
|
|
33
|
+
// libs/storage/src/lib/storage.types.ts
|
|
34
|
+
var init_storage_types = __esm({
|
|
35
|
+
"libs/storage/src/lib/storage.types.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// libs/storage/src/lib/storage-local.ts
|
|
41
|
+
function localStorageAdapter(options) {
|
|
42
|
+
const { directory, baseUrl } = options;
|
|
43
|
+
const resolvedRoot = (0, import_node_path.resolve)(directory);
|
|
44
|
+
if (!(0, import_node_fs.existsSync)(resolvedRoot)) {
|
|
45
|
+
(0, import_node_fs.mkdirSync)(resolvedRoot, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
function safePath(unsafePath) {
|
|
48
|
+
const normalized = (0, import_node_path.normalize)(unsafePath);
|
|
49
|
+
if (normalized.includes("..")) {
|
|
50
|
+
throw new Error("Invalid path: directory traversal not allowed");
|
|
51
|
+
}
|
|
52
|
+
const full = (0, import_node_path.resolve)(resolvedRoot, normalized);
|
|
53
|
+
if (!full.startsWith(resolvedRoot)) {
|
|
54
|
+
throw new Error("Invalid path: directory traversal not allowed");
|
|
55
|
+
}
|
|
56
|
+
if ((0, import_node_fs.existsSync)(full) && (0, import_node_fs.lstatSync)(full).isSymbolicLink()) {
|
|
57
|
+
throw new Error("Invalid path: symbolic links not allowed");
|
|
58
|
+
}
|
|
59
|
+
return full;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
async upload(file, uploadOptions) {
|
|
63
|
+
const ext = (0, import_node_path.extname)(file.originalName) || getExtensionFromMimeType(file.mimeType);
|
|
64
|
+
const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${(0, import_node_crypto2.randomUUID)()}${ext}`;
|
|
65
|
+
const subdir = uploadOptions?.directory ?? "";
|
|
66
|
+
const targetDir = subdir ? safePath(subdir) : resolvedRoot;
|
|
67
|
+
if (subdir && !(0, import_node_fs.existsSync)(targetDir)) {
|
|
68
|
+
(0, import_node_fs.mkdirSync)(targetDir, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
const filePath = safePath(subdir ? (0, import_node_path.join)(subdir, filename) : filename);
|
|
71
|
+
const relativePath = subdir ? (0, import_node_path.join)((0, import_node_path.normalize)(subdir), filename) : filename;
|
|
72
|
+
(0, import_node_fs.writeFileSync)(filePath, file.buffer);
|
|
73
|
+
const url = baseUrl ? `${baseUrl}/${relativePath}` : `/api/media/file/${relativePath}`;
|
|
74
|
+
return {
|
|
75
|
+
path: relativePath,
|
|
76
|
+
url,
|
|
77
|
+
filename,
|
|
78
|
+
mimeType: file.mimeType,
|
|
79
|
+
size: file.size
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
async delete(path) {
|
|
83
|
+
const filePath = safePath(path);
|
|
84
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
85
|
+
(0, import_node_fs.unlinkSync)(filePath);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
},
|
|
90
|
+
getUrl(path) {
|
|
91
|
+
return baseUrl ? `${baseUrl}/${path}` : `/api/media/file/${path}`;
|
|
92
|
+
},
|
|
93
|
+
async exists(path) {
|
|
94
|
+
const filePath = safePath(path);
|
|
95
|
+
return (0, import_node_fs.existsSync)(filePath);
|
|
96
|
+
},
|
|
97
|
+
async read(path) {
|
|
98
|
+
const filePath = safePath(path);
|
|
99
|
+
if ((0, import_node_fs.existsSync)(filePath)) {
|
|
100
|
+
return (0, import_node_fs.readFileSync)(filePath);
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function getExtensionFromMimeType(mimeType) {
|
|
107
|
+
const mimeToExt = {
|
|
108
|
+
"image/jpeg": ".jpg",
|
|
109
|
+
"image/png": ".png",
|
|
110
|
+
"image/gif": ".gif",
|
|
111
|
+
"image/webp": ".webp",
|
|
112
|
+
"image/svg+xml": ".svg",
|
|
113
|
+
"application/pdf": ".pdf",
|
|
114
|
+
"application/json": ".json",
|
|
115
|
+
"text/plain": ".txt",
|
|
116
|
+
"text/html": ".html",
|
|
117
|
+
"text/css": ".css",
|
|
118
|
+
"application/javascript": ".js",
|
|
119
|
+
"video/mp4": ".mp4",
|
|
120
|
+
"video/webm": ".webm",
|
|
121
|
+
"audio/mpeg": ".mp3",
|
|
122
|
+
"audio/wav": ".wav",
|
|
123
|
+
"application/zip": ".zip"
|
|
124
|
+
};
|
|
125
|
+
return mimeToExt[mimeType] ?? "";
|
|
126
|
+
}
|
|
127
|
+
var import_node_fs, import_node_path, import_node_crypto2;
|
|
128
|
+
var init_storage_local = __esm({
|
|
129
|
+
"libs/storage/src/lib/storage-local.ts"() {
|
|
130
|
+
"use strict";
|
|
131
|
+
import_node_fs = require("node:fs");
|
|
132
|
+
import_node_path = require("node:path");
|
|
133
|
+
import_node_crypto2 = require("node:crypto");
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// libs/storage/src/lib/storage-s3.ts
|
|
138
|
+
async function loadAwsSdk() {
|
|
139
|
+
if (S3Client)
|
|
140
|
+
return;
|
|
141
|
+
const s3Module = await import("@aws-sdk/client-s3");
|
|
142
|
+
const presignerModule = await import("@aws-sdk/s3-request-presigner");
|
|
143
|
+
S3Client = s3Module.S3Client;
|
|
144
|
+
PutObjectCommand = s3Module.PutObjectCommand;
|
|
145
|
+
DeleteObjectCommand = s3Module.DeleteObjectCommand;
|
|
146
|
+
HeadObjectCommand = s3Module.HeadObjectCommand;
|
|
147
|
+
GetObjectCommand = s3Module.GetObjectCommand;
|
|
148
|
+
getSignedUrl = presignerModule.getSignedUrl;
|
|
149
|
+
}
|
|
150
|
+
function s3StorageAdapter(options) {
|
|
151
|
+
const {
|
|
152
|
+
bucket,
|
|
153
|
+
region,
|
|
154
|
+
accessKeyId,
|
|
155
|
+
secretAccessKey,
|
|
156
|
+
endpoint,
|
|
157
|
+
baseUrl,
|
|
158
|
+
forcePathStyle = false,
|
|
159
|
+
acl = "private",
|
|
160
|
+
presignedUrlExpiry = 3600
|
|
161
|
+
} = options;
|
|
162
|
+
let client = null;
|
|
163
|
+
async function getClient() {
|
|
164
|
+
await loadAwsSdk();
|
|
165
|
+
if (!client) {
|
|
166
|
+
const clientConfig = {
|
|
167
|
+
region,
|
|
168
|
+
forcePathStyle
|
|
169
|
+
};
|
|
170
|
+
if (endpoint) {
|
|
171
|
+
clientConfig.endpoint = endpoint;
|
|
172
|
+
}
|
|
173
|
+
if (accessKeyId && secretAccessKey) {
|
|
174
|
+
clientConfig.credentials = {
|
|
175
|
+
accessKeyId,
|
|
176
|
+
secretAccessKey
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
client = new S3Client(clientConfig);
|
|
180
|
+
}
|
|
181
|
+
return client;
|
|
182
|
+
}
|
|
183
|
+
function getPublicUrl(key) {
|
|
184
|
+
if (baseUrl) {
|
|
185
|
+
return `${baseUrl}/${key}`;
|
|
186
|
+
}
|
|
187
|
+
if (endpoint) {
|
|
188
|
+
return `${endpoint}/${bucket}/${key}`;
|
|
189
|
+
}
|
|
190
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
async upload(file, uploadOptions) {
|
|
194
|
+
const s3 = await getClient();
|
|
195
|
+
const ext = (0, import_node_path2.extname)(file.originalName) || getExtensionFromMimeType2(file.mimeType);
|
|
196
|
+
const filename = uploadOptions?.filename ? `${uploadOptions.filename}${ext}` : `${(0, import_node_crypto3.randomUUID)()}${ext}`;
|
|
197
|
+
const key = uploadOptions?.directory ? `${uploadOptions.directory}/${filename}` : filename;
|
|
198
|
+
await s3.send(
|
|
199
|
+
new PutObjectCommand({
|
|
200
|
+
Bucket: bucket,
|
|
201
|
+
Key: key,
|
|
202
|
+
Body: file.buffer,
|
|
203
|
+
ContentType: file.mimeType,
|
|
204
|
+
ACL: acl
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
path: key,
|
|
209
|
+
url: acl === "public-read" ? getPublicUrl(key) : `/api/media/file/${key}`,
|
|
210
|
+
filename,
|
|
211
|
+
mimeType: file.mimeType,
|
|
212
|
+
size: file.size
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
async delete(path) {
|
|
216
|
+
const s3 = await getClient();
|
|
217
|
+
try {
|
|
218
|
+
await s3.send(
|
|
219
|
+
new DeleteObjectCommand({
|
|
220
|
+
Bucket: bucket,
|
|
221
|
+
Key: path
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
getUrl(path) {
|
|
230
|
+
if (acl === "public-read") {
|
|
231
|
+
return getPublicUrl(path);
|
|
232
|
+
}
|
|
233
|
+
return `/api/media/file/${path}`;
|
|
234
|
+
},
|
|
235
|
+
async exists(path) {
|
|
236
|
+
const s3 = await getClient();
|
|
237
|
+
try {
|
|
238
|
+
await s3.send(
|
|
239
|
+
new HeadObjectCommand({
|
|
240
|
+
Bucket: bucket,
|
|
241
|
+
Key: path
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
return true;
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
async getSignedUrl(path, expiresIn) {
|
|
250
|
+
const s3 = await getClient();
|
|
251
|
+
await loadAwsSdk();
|
|
252
|
+
const command = new GetObjectCommand({
|
|
253
|
+
Bucket: bucket,
|
|
254
|
+
Key: path
|
|
255
|
+
});
|
|
256
|
+
return getSignedUrl(s3, command, {
|
|
257
|
+
expiresIn: expiresIn ?? presignedUrlExpiry
|
|
258
|
+
});
|
|
259
|
+
},
|
|
260
|
+
async read(path) {
|
|
261
|
+
const s3 = await getClient();
|
|
262
|
+
try {
|
|
263
|
+
const response = await s3.send(
|
|
264
|
+
new GetObjectCommand({
|
|
265
|
+
Bucket: bucket,
|
|
266
|
+
Key: path
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
if (!response.Body) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const chunks = [];
|
|
273
|
+
const body = response.Body;
|
|
274
|
+
for await (const chunk of body) {
|
|
275
|
+
chunks.push(chunk);
|
|
276
|
+
}
|
|
277
|
+
return Buffer.concat(chunks);
|
|
278
|
+
} catch {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function getExtensionFromMimeType2(mimeType) {
|
|
285
|
+
const mimeToExt = {
|
|
286
|
+
"image/jpeg": ".jpg",
|
|
287
|
+
"image/png": ".png",
|
|
288
|
+
"image/gif": ".gif",
|
|
289
|
+
"image/webp": ".webp",
|
|
290
|
+
"image/svg+xml": ".svg",
|
|
291
|
+
"application/pdf": ".pdf",
|
|
292
|
+
"application/json": ".json",
|
|
293
|
+
"text/plain": ".txt",
|
|
294
|
+
"text/html": ".html",
|
|
295
|
+
"text/css": ".css",
|
|
296
|
+
"application/javascript": ".js",
|
|
297
|
+
"video/mp4": ".mp4",
|
|
298
|
+
"video/webm": ".webm",
|
|
299
|
+
"audio/mpeg": ".mp3",
|
|
300
|
+
"audio/wav": ".wav",
|
|
301
|
+
"application/zip": ".zip"
|
|
302
|
+
};
|
|
303
|
+
return mimeToExt[mimeType] ?? "";
|
|
304
|
+
}
|
|
305
|
+
var import_node_crypto3, import_node_path2, S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, GetObjectCommand, getSignedUrl;
|
|
306
|
+
var init_storage_s3 = __esm({
|
|
307
|
+
"libs/storage/src/lib/storage-s3.ts"() {
|
|
308
|
+
"use strict";
|
|
309
|
+
import_node_crypto3 = require("node:crypto");
|
|
310
|
+
import_node_path2 = require("node:path");
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// libs/storage/src/lib/mime-validator.ts
|
|
315
|
+
function detectMimeType(buffer) {
|
|
316
|
+
for (const sig of FILE_SIGNATURES) {
|
|
317
|
+
const offset = sig.offset ?? 0;
|
|
318
|
+
if (buffer.length < offset + sig.bytes.length) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
let match = true;
|
|
322
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
323
|
+
if (buffer[offset + i] !== sig.bytes[i]) {
|
|
324
|
+
match = false;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (match) {
|
|
329
|
+
if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
|
|
330
|
+
if (buffer.length >= 12) {
|
|
331
|
+
const formatId = buffer.slice(8, 12).toString("ascii");
|
|
332
|
+
if (formatId === "WEBP") {
|
|
333
|
+
return "image/webp";
|
|
334
|
+
}
|
|
335
|
+
if (formatId === "WAVE") {
|
|
336
|
+
return "audio/wav";
|
|
337
|
+
}
|
|
338
|
+
if (formatId === "AVI ") {
|
|
339
|
+
return "video/avi";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
|
|
344
|
+
const boxType = buffer.slice(4, 8).toString("ascii");
|
|
345
|
+
if (boxType === "ftyp") {
|
|
346
|
+
return "video/mp4";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return sig.mimeType;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (isTextContent(buffer)) {
|
|
353
|
+
const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
|
|
354
|
+
if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
|
|
355
|
+
return "application/json";
|
|
356
|
+
}
|
|
357
|
+
if (text2.trim().startsWith("<")) {
|
|
358
|
+
if (text2.includes("<svg")) {
|
|
359
|
+
return "image/svg+xml";
|
|
360
|
+
}
|
|
361
|
+
if (text2.includes("<!DOCTYPE html") || text2.includes("<html")) {
|
|
362
|
+
return "text/html";
|
|
363
|
+
}
|
|
364
|
+
return "application/xml";
|
|
365
|
+
}
|
|
366
|
+
return "text/plain";
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
function isTextContent(buffer) {
|
|
371
|
+
const checkLength = Math.min(buffer.length, 512);
|
|
372
|
+
for (let i = 0; i < checkLength; i++) {
|
|
373
|
+
const byte = buffer[i];
|
|
374
|
+
if (byte < 9 || // Control chars before tab
|
|
375
|
+
byte > 13 && byte < 32 || // Control chars between CR and space
|
|
376
|
+
byte === 127) {
|
|
377
|
+
if (byte >= 128 && byte <= 191) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (byte >= 192 && byte <= 247) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
function mimeTypeMatches(mimeType, pattern) {
|
|
389
|
+
if (pattern === "*" || pattern === "*/*") {
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
if (pattern.endsWith("/*")) {
|
|
393
|
+
const category = pattern.slice(0, -2);
|
|
394
|
+
return mimeType.startsWith(`${category}/`);
|
|
395
|
+
}
|
|
396
|
+
return mimeType === pattern;
|
|
397
|
+
}
|
|
398
|
+
function isMimeTypeAllowed(mimeType, allowedTypes) {
|
|
399
|
+
if (allowedTypes.length === 0) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
return allowedTypes.some((pattern) => mimeTypeMatches(mimeType, pattern));
|
|
403
|
+
}
|
|
404
|
+
function validateMimeType(buffer, claimedType, allowedTypes) {
|
|
405
|
+
const detectedType = detectMimeType(buffer);
|
|
406
|
+
if (allowedTypes && allowedTypes.length > 0) {
|
|
407
|
+
const typeToCheck = detectedType ?? claimedType;
|
|
408
|
+
if (!isMimeTypeAllowed(typeToCheck, allowedTypes)) {
|
|
409
|
+
return {
|
|
410
|
+
valid: false,
|
|
411
|
+
detectedType,
|
|
412
|
+
claimedType,
|
|
413
|
+
error: `File type '${typeToCheck}' is not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!detectedType) {
|
|
418
|
+
return {
|
|
419
|
+
valid: true,
|
|
420
|
+
detectedType: null,
|
|
421
|
+
claimedType
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
const compatible = areMimeTypesCompatible(detectedType, claimedType);
|
|
425
|
+
if (!compatible) {
|
|
426
|
+
return {
|
|
427
|
+
valid: false,
|
|
428
|
+
detectedType,
|
|
429
|
+
claimedType,
|
|
430
|
+
error: `File appears to be '${detectedType}' but was uploaded as '${claimedType}'`
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
valid: true,
|
|
435
|
+
detectedType,
|
|
436
|
+
claimedType
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function areMimeTypesCompatible(detected, claimed) {
|
|
440
|
+
if (detected === claimed) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
const [detectedCategory] = detected.split("/");
|
|
444
|
+
const [claimedCategory] = claimed.split("/");
|
|
445
|
+
if (detectedCategory !== claimedCategory) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
const variations = {
|
|
449
|
+
"image/jpeg": ["image/jpg", "image/pjpeg"],
|
|
450
|
+
"text/plain": ["text/x-plain"],
|
|
451
|
+
"application/json": ["text/json"],
|
|
452
|
+
"application/javascript": ["text/javascript", "application/x-javascript"]
|
|
453
|
+
};
|
|
454
|
+
const allowedVariations = variations[detected];
|
|
455
|
+
if (allowedVariations && allowedVariations.includes(claimed)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
for (const [canonical, variants] of Object.entries(variations)) {
|
|
459
|
+
if (variants.includes(detected) && (canonical === claimed || variants.includes(claimed))) {
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
var FILE_SIGNATURES;
|
|
466
|
+
var init_mime_validator = __esm({
|
|
467
|
+
"libs/storage/src/lib/mime-validator.ts"() {
|
|
468
|
+
"use strict";
|
|
469
|
+
FILE_SIGNATURES = [
|
|
470
|
+
// Images
|
|
471
|
+
{ mimeType: "image/jpeg", bytes: [255, 216, 255] },
|
|
472
|
+
{ mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
|
|
473
|
+
{ mimeType: "image/gif", bytes: [71, 73, 70, 56] },
|
|
474
|
+
// GIF8
|
|
475
|
+
{ mimeType: "image/webp", bytes: [82, 73, 70, 70], offset: 0 },
|
|
476
|
+
// RIFF (need to check for WEBP at offset 8)
|
|
477
|
+
{ mimeType: "image/bmp", bytes: [66, 77] },
|
|
478
|
+
// BM
|
|
479
|
+
{ mimeType: "image/tiff", bytes: [73, 73, 42, 0] },
|
|
480
|
+
// Little-endian TIFF
|
|
481
|
+
{ mimeType: "image/tiff", bytes: [77, 77, 0, 42] },
|
|
482
|
+
// Big-endian TIFF
|
|
483
|
+
{ mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
|
|
484
|
+
// ICO
|
|
485
|
+
{ mimeType: "image/svg+xml", bytes: [60, 115, 118, 103] },
|
|
486
|
+
// <svg (partial match)
|
|
487
|
+
// Documents
|
|
488
|
+
{ mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
|
|
489
|
+
// %PDF
|
|
490
|
+
// Archives
|
|
491
|
+
{ mimeType: "application/zip", bytes: [80, 75, 3, 4] },
|
|
492
|
+
// PK
|
|
493
|
+
{ mimeType: "application/gzip", bytes: [31, 139] },
|
|
494
|
+
{ mimeType: "application/x-rar-compressed", bytes: [82, 97, 114, 33] },
|
|
495
|
+
// Rar!
|
|
496
|
+
// Audio
|
|
497
|
+
{ mimeType: "audio/mpeg", bytes: [73, 68, 51] },
|
|
498
|
+
// ID3 (MP3)
|
|
499
|
+
{ mimeType: "audio/mpeg", bytes: [255, 251] },
|
|
500
|
+
// MP3 frame sync
|
|
501
|
+
{ mimeType: "audio/wav", bytes: [82, 73, 70, 70] },
|
|
502
|
+
// RIFF (need to check for WAVE)
|
|
503
|
+
{ mimeType: "audio/ogg", bytes: [79, 103, 103, 83] },
|
|
504
|
+
// OggS
|
|
505
|
+
{ mimeType: "audio/flac", bytes: [102, 76, 97, 67] },
|
|
506
|
+
// fLaC
|
|
507
|
+
// Video
|
|
508
|
+
{ mimeType: "video/mp4", bytes: [0, 0, 0], offset: 0 },
|
|
509
|
+
// Need to check for ftyp at offset 4
|
|
510
|
+
{ mimeType: "video/webm", bytes: [26, 69, 223, 163] },
|
|
511
|
+
// EBML header
|
|
512
|
+
{ mimeType: "video/avi", bytes: [82, 73, 70, 70] },
|
|
513
|
+
// RIFF (need to check for AVI)
|
|
514
|
+
// Executables (for blocking)
|
|
515
|
+
{ mimeType: "application/x-executable", bytes: [127, 69, 76, 70] },
|
|
516
|
+
// ELF
|
|
517
|
+
{ mimeType: "application/x-msdownload", bytes: [77, 90] }
|
|
518
|
+
// MZ (Windows EXE)
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// libs/storage/src/index.ts
|
|
31
524
|
var src_exports = {};
|
|
32
525
|
__export(src_exports, {
|
|
526
|
+
detectMimeType: () => detectMimeType,
|
|
527
|
+
isMimeTypeAllowed: () => isMimeTypeAllowed,
|
|
528
|
+
localStorageAdapter: () => localStorageAdapter,
|
|
529
|
+
mimeTypeMatches: () => mimeTypeMatches,
|
|
530
|
+
s3StorageAdapter: () => s3StorageAdapter,
|
|
531
|
+
validateMimeType: () => validateMimeType
|
|
532
|
+
});
|
|
533
|
+
var init_src = __esm({
|
|
534
|
+
"libs/storage/src/index.ts"() {
|
|
535
|
+
"use strict";
|
|
536
|
+
init_storage_types();
|
|
537
|
+
init_storage_local();
|
|
538
|
+
init_storage_s3();
|
|
539
|
+
init_mime_validator();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// libs/server-analog/src/index.ts
|
|
544
|
+
var src_exports2 = {};
|
|
545
|
+
__export(src_exports2, {
|
|
33
546
|
createComprehensiveMomentumHandler: () => createComprehensiveMomentumHandler,
|
|
34
547
|
createMomentumHandler: () => createMomentumHandler,
|
|
35
548
|
createSimpleMomentumHandler: () => createSimpleMomentumHandler
|
|
36
549
|
});
|
|
37
|
-
module.exports = __toCommonJS(
|
|
550
|
+
module.exports = __toCommonJS(src_exports2);
|
|
38
551
|
|
|
39
552
|
// libs/logger/src/lib/log-level.ts
|
|
40
553
|
var LOG_LEVEL_VALUES = {
|
|
@@ -291,6 +804,9 @@ function getSoftDeleteField(config) {
|
|
|
291
804
|
const sdConfig = config.softDelete;
|
|
292
805
|
return sdConfig.field ?? "deletedAt";
|
|
293
806
|
}
|
|
807
|
+
function isUploadCollection(config) {
|
|
808
|
+
return config.upload != null;
|
|
809
|
+
}
|
|
294
810
|
|
|
295
811
|
// libs/core/src/lib/fields/field.types.ts
|
|
296
812
|
var ReferentialIntegrityError = class extends Error {
|
|
@@ -2970,210 +3486,8 @@ function createAdapterApiKeyStore(adapter) {
|
|
|
2970
3486
|
};
|
|
2971
3487
|
}
|
|
2972
3488
|
|
|
2973
|
-
// libs/storage/src/lib/mime-validator.ts
|
|
2974
|
-
var FILE_SIGNATURES = [
|
|
2975
|
-
// Images
|
|
2976
|
-
{ mimeType: "image/jpeg", bytes: [255, 216, 255] },
|
|
2977
|
-
{ mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
|
|
2978
|
-
{ mimeType: "image/gif", bytes: [71, 73, 70, 56] },
|
|
2979
|
-
// GIF8
|
|
2980
|
-
{ mimeType: "image/webp", bytes: [82, 73, 70, 70], offset: 0 },
|
|
2981
|
-
// RIFF (need to check for WEBP at offset 8)
|
|
2982
|
-
{ mimeType: "image/bmp", bytes: [66, 77] },
|
|
2983
|
-
// BM
|
|
2984
|
-
{ mimeType: "image/tiff", bytes: [73, 73, 42, 0] },
|
|
2985
|
-
// Little-endian TIFF
|
|
2986
|
-
{ mimeType: "image/tiff", bytes: [77, 77, 0, 42] },
|
|
2987
|
-
// Big-endian TIFF
|
|
2988
|
-
{ mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
|
|
2989
|
-
// ICO
|
|
2990
|
-
{ mimeType: "image/svg+xml", bytes: [60, 115, 118, 103] },
|
|
2991
|
-
// <svg (partial match)
|
|
2992
|
-
// Documents
|
|
2993
|
-
{ mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
|
|
2994
|
-
// %PDF
|
|
2995
|
-
// Archives
|
|
2996
|
-
{ mimeType: "application/zip", bytes: [80, 75, 3, 4] },
|
|
2997
|
-
// PK
|
|
2998
|
-
{ mimeType: "application/gzip", bytes: [31, 139] },
|
|
2999
|
-
{ mimeType: "application/x-rar-compressed", bytes: [82, 97, 114, 33] },
|
|
3000
|
-
// Rar!
|
|
3001
|
-
// Audio
|
|
3002
|
-
{ mimeType: "audio/mpeg", bytes: [73, 68, 51] },
|
|
3003
|
-
// ID3 (MP3)
|
|
3004
|
-
{ mimeType: "audio/mpeg", bytes: [255, 251] },
|
|
3005
|
-
// MP3 frame sync
|
|
3006
|
-
{ mimeType: "audio/wav", bytes: [82, 73, 70, 70] },
|
|
3007
|
-
// RIFF (need to check for WAVE)
|
|
3008
|
-
{ mimeType: "audio/ogg", bytes: [79, 103, 103, 83] },
|
|
3009
|
-
// OggS
|
|
3010
|
-
{ mimeType: "audio/flac", bytes: [102, 76, 97, 67] },
|
|
3011
|
-
// fLaC
|
|
3012
|
-
// Video
|
|
3013
|
-
{ mimeType: "video/mp4", bytes: [0, 0, 0], offset: 0 },
|
|
3014
|
-
// Need to check for ftyp at offset 4
|
|
3015
|
-
{ mimeType: "video/webm", bytes: [26, 69, 223, 163] },
|
|
3016
|
-
// EBML header
|
|
3017
|
-
{ mimeType: "video/avi", bytes: [82, 73, 70, 70] },
|
|
3018
|
-
// RIFF (need to check for AVI)
|
|
3019
|
-
// Executables (for blocking)
|
|
3020
|
-
{ mimeType: "application/x-executable", bytes: [127, 69, 76, 70] },
|
|
3021
|
-
// ELF
|
|
3022
|
-
{ mimeType: "application/x-msdownload", bytes: [77, 90] }
|
|
3023
|
-
// MZ (Windows EXE)
|
|
3024
|
-
];
|
|
3025
|
-
function detectMimeType(buffer) {
|
|
3026
|
-
for (const sig of FILE_SIGNATURES) {
|
|
3027
|
-
const offset = sig.offset ?? 0;
|
|
3028
|
-
if (buffer.length < offset + sig.bytes.length) {
|
|
3029
|
-
continue;
|
|
3030
|
-
}
|
|
3031
|
-
let match = true;
|
|
3032
|
-
for (let i = 0; i < sig.bytes.length; i++) {
|
|
3033
|
-
if (buffer[offset + i] !== sig.bytes[i]) {
|
|
3034
|
-
match = false;
|
|
3035
|
-
break;
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
if (match) {
|
|
3039
|
-
if (sig.bytes[0] === 82 && sig.bytes[1] === 73) {
|
|
3040
|
-
if (buffer.length >= 12) {
|
|
3041
|
-
const formatId = buffer.slice(8, 12).toString("ascii");
|
|
3042
|
-
if (formatId === "WEBP") {
|
|
3043
|
-
return "image/webp";
|
|
3044
|
-
}
|
|
3045
|
-
if (formatId === "WAVE") {
|
|
3046
|
-
return "audio/wav";
|
|
3047
|
-
}
|
|
3048
|
-
if (formatId === "AVI ") {
|
|
3049
|
-
return "video/avi";
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
}
|
|
3053
|
-
if (sig.mimeType === "video/mp4" && buffer.length >= 8) {
|
|
3054
|
-
const boxType = buffer.slice(4, 8).toString("ascii");
|
|
3055
|
-
if (boxType === "ftyp") {
|
|
3056
|
-
return "video/mp4";
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
return sig.mimeType;
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
if (isTextContent(buffer)) {
|
|
3063
|
-
const text2 = buffer.toString("utf8", 0, Math.min(buffer.length, 1e3));
|
|
3064
|
-
if (text2.trim().startsWith("{") || text2.trim().startsWith("[")) {
|
|
3065
|
-
return "application/json";
|
|
3066
|
-
}
|
|
3067
|
-
if (text2.trim().startsWith("<")) {
|
|
3068
|
-
if (text2.includes("<svg")) {
|
|
3069
|
-
return "image/svg+xml";
|
|
3070
|
-
}
|
|
3071
|
-
if (text2.includes("<!DOCTYPE html") || text2.includes("<html")) {
|
|
3072
|
-
return "text/html";
|
|
3073
|
-
}
|
|
3074
|
-
return "application/xml";
|
|
3075
|
-
}
|
|
3076
|
-
return "text/plain";
|
|
3077
|
-
}
|
|
3078
|
-
return null;
|
|
3079
|
-
}
|
|
3080
|
-
function isTextContent(buffer) {
|
|
3081
|
-
const checkLength = Math.min(buffer.length, 512);
|
|
3082
|
-
for (let i = 0; i < checkLength; i++) {
|
|
3083
|
-
const byte = buffer[i];
|
|
3084
|
-
if (byte < 9 || // Control chars before tab
|
|
3085
|
-
byte > 13 && byte < 32 || // Control chars between CR and space
|
|
3086
|
-
byte === 127) {
|
|
3087
|
-
if (byte >= 128 && byte <= 191) {
|
|
3088
|
-
continue;
|
|
3089
|
-
}
|
|
3090
|
-
if (byte >= 192 && byte <= 247) {
|
|
3091
|
-
continue;
|
|
3092
|
-
}
|
|
3093
|
-
return false;
|
|
3094
|
-
}
|
|
3095
|
-
}
|
|
3096
|
-
return true;
|
|
3097
|
-
}
|
|
3098
|
-
function mimeTypeMatches(mimeType, pattern) {
|
|
3099
|
-
if (pattern === "*" || pattern === "*/*") {
|
|
3100
|
-
return true;
|
|
3101
|
-
}
|
|
3102
|
-
if (pattern.endsWith("/*")) {
|
|
3103
|
-
const category = pattern.slice(0, -2);
|
|
3104
|
-
return mimeType.startsWith(`${category}/`);
|
|
3105
|
-
}
|
|
3106
|
-
return mimeType === pattern;
|
|
3107
|
-
}
|
|
3108
|
-
function isMimeTypeAllowed(mimeType, allowedTypes) {
|
|
3109
|
-
if (allowedTypes.length === 0) {
|
|
3110
|
-
return true;
|
|
3111
|
-
}
|
|
3112
|
-
return allowedTypes.some((pattern) => mimeTypeMatches(mimeType, pattern));
|
|
3113
|
-
}
|
|
3114
|
-
function validateMimeType(buffer, claimedType, allowedTypes) {
|
|
3115
|
-
const detectedType = detectMimeType(buffer);
|
|
3116
|
-
if (allowedTypes && allowedTypes.length > 0) {
|
|
3117
|
-
const typeToCheck = detectedType ?? claimedType;
|
|
3118
|
-
if (!isMimeTypeAllowed(typeToCheck, allowedTypes)) {
|
|
3119
|
-
return {
|
|
3120
|
-
valid: false,
|
|
3121
|
-
detectedType,
|
|
3122
|
-
claimedType,
|
|
3123
|
-
error: `File type '${typeToCheck}' is not allowed. Allowed types: ${allowedTypes.join(", ")}`
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
if (!detectedType) {
|
|
3128
|
-
return {
|
|
3129
|
-
valid: true,
|
|
3130
|
-
detectedType: null,
|
|
3131
|
-
claimedType
|
|
3132
|
-
};
|
|
3133
|
-
}
|
|
3134
|
-
const compatible = areMimeTypesCompatible(detectedType, claimedType);
|
|
3135
|
-
if (!compatible) {
|
|
3136
|
-
return {
|
|
3137
|
-
valid: false,
|
|
3138
|
-
detectedType,
|
|
3139
|
-
claimedType,
|
|
3140
|
-
error: `File appears to be '${detectedType}' but was uploaded as '${claimedType}'`
|
|
3141
|
-
};
|
|
3142
|
-
}
|
|
3143
|
-
return {
|
|
3144
|
-
valid: true,
|
|
3145
|
-
detectedType,
|
|
3146
|
-
claimedType
|
|
3147
|
-
};
|
|
3148
|
-
}
|
|
3149
|
-
function areMimeTypesCompatible(detected, claimed) {
|
|
3150
|
-
if (detected === claimed) {
|
|
3151
|
-
return true;
|
|
3152
|
-
}
|
|
3153
|
-
const [detectedCategory] = detected.split("/");
|
|
3154
|
-
const [claimedCategory] = claimed.split("/");
|
|
3155
|
-
if (detectedCategory !== claimedCategory) {
|
|
3156
|
-
return false;
|
|
3157
|
-
}
|
|
3158
|
-
const variations = {
|
|
3159
|
-
"image/jpeg": ["image/jpg", "image/pjpeg"],
|
|
3160
|
-
"text/plain": ["text/x-plain"],
|
|
3161
|
-
"application/json": ["text/json"],
|
|
3162
|
-
"application/javascript": ["text/javascript", "application/x-javascript"]
|
|
3163
|
-
};
|
|
3164
|
-
const allowedVariations = variations[detected];
|
|
3165
|
-
if (allowedVariations && allowedVariations.includes(claimed)) {
|
|
3166
|
-
return true;
|
|
3167
|
-
}
|
|
3168
|
-
for (const [canonical, variants] of Object.entries(variations)) {
|
|
3169
|
-
if (variants.includes(detected) && (canonical === claimed || variants.includes(claimed))) {
|
|
3170
|
-
return true;
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
return false;
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
3489
|
// libs/server-core/src/lib/upload-handler.ts
|
|
3490
|
+
init_src();
|
|
3177
3491
|
function getUploadConfig(config) {
|
|
3178
3492
|
if (!config.storage?.adapter) {
|
|
3179
3493
|
return null;
|
|
@@ -3288,6 +3602,64 @@ async function handleUpload(config, request) {
|
|
|
3288
3602
|
};
|
|
3289
3603
|
}
|
|
3290
3604
|
}
|
|
3605
|
+
async function handleCollectionUpload(globalConfig, request) {
|
|
3606
|
+
const { adapter } = globalConfig;
|
|
3607
|
+
const { file, user, fields, collectionSlug, collectionUpload } = request;
|
|
3608
|
+
const maxFileSize = collectionUpload.maxFileSize ?? globalConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
3609
|
+
const allowedMimeTypes = collectionUpload.mimeTypes ?? globalConfig.allowedMimeTypes ?? [];
|
|
3610
|
+
try {
|
|
3611
|
+
if (!user) {
|
|
3612
|
+
return {
|
|
3613
|
+
status: 401,
|
|
3614
|
+
error: "Authentication required to upload files"
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
const sizeError = validateFileSize(file, maxFileSize);
|
|
3618
|
+
if (sizeError) {
|
|
3619
|
+
return { status: 400, error: sizeError };
|
|
3620
|
+
}
|
|
3621
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
3622
|
+
if (mimeError) {
|
|
3623
|
+
return { status: 400, error: mimeError };
|
|
3624
|
+
}
|
|
3625
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
3626
|
+
const magicByteResult = validateMimeType(
|
|
3627
|
+
file.buffer,
|
|
3628
|
+
file.mimeType,
|
|
3629
|
+
allowedMimeTypes
|
|
3630
|
+
);
|
|
3631
|
+
if (!magicByteResult.valid) {
|
|
3632
|
+
return {
|
|
3633
|
+
status: 400,
|
|
3634
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
const storedFile = await adapter.upload(file);
|
|
3639
|
+
const docData = {
|
|
3640
|
+
...fields,
|
|
3641
|
+
filename: file.originalName,
|
|
3642
|
+
mimeType: file.mimeType,
|
|
3643
|
+
filesize: file.size,
|
|
3644
|
+
path: storedFile.path,
|
|
3645
|
+
url: storedFile.url
|
|
3646
|
+
};
|
|
3647
|
+
const api = getMomentumAPI().setContext({ user });
|
|
3648
|
+
const doc = await api.collection(collectionSlug).create(docData);
|
|
3649
|
+
return {
|
|
3650
|
+
status: 201,
|
|
3651
|
+
doc
|
|
3652
|
+
};
|
|
3653
|
+
} catch (error) {
|
|
3654
|
+
if (error instanceof Error) {
|
|
3655
|
+
if (error.message.includes("Access denied")) {
|
|
3656
|
+
return { status: 403, error: error.message };
|
|
3657
|
+
}
|
|
3658
|
+
return { status: 500, error: `Upload failed: ${error.message}` };
|
|
3659
|
+
}
|
|
3660
|
+
return { status: 500, error: "Upload failed: Unknown error" };
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3291
3663
|
async function handleFileGet(adapter, path) {
|
|
3292
3664
|
if (!adapter.read) {
|
|
3293
3665
|
return null;
|
|
@@ -4370,6 +4742,39 @@ function coerceCsvValue(value, fieldType) {
|
|
|
4370
4742
|
}
|
|
4371
4743
|
|
|
4372
4744
|
// libs/server-analog/src/lib/server-analog.ts
|
|
4745
|
+
function nestBracketParams(flat) {
|
|
4746
|
+
const result = {};
|
|
4747
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
4748
|
+
const bracketIdx = key.indexOf("[");
|
|
4749
|
+
if (bracketIdx === -1) {
|
|
4750
|
+
result[key] = value;
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
const rootKey = key.slice(0, bracketIdx);
|
|
4754
|
+
const bracketPart = key.slice(bracketIdx);
|
|
4755
|
+
const parts = [];
|
|
4756
|
+
const bracketRegex = /\[([^\]]*)\]/g;
|
|
4757
|
+
let m;
|
|
4758
|
+
while ((m = bracketRegex.exec(bracketPart)) !== null) {
|
|
4759
|
+
parts.push(m[1]);
|
|
4760
|
+
}
|
|
4761
|
+
if (parts.length === 0) {
|
|
4762
|
+
result[key] = value;
|
|
4763
|
+
continue;
|
|
4764
|
+
}
|
|
4765
|
+
let current = result[rootKey] ?? {};
|
|
4766
|
+
result[rootKey] = current;
|
|
4767
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
4768
|
+
const part = parts[i];
|
|
4769
|
+
if (typeof current[part] !== "object" || current[part] === null) {
|
|
4770
|
+
current[part] = {};
|
|
4771
|
+
}
|
|
4772
|
+
current = current[part];
|
|
4773
|
+
}
|
|
4774
|
+
current[parts[parts.length - 1]] = value;
|
|
4775
|
+
}
|
|
4776
|
+
return result;
|
|
4777
|
+
}
|
|
4373
4778
|
function toMomentumMethod(m) {
|
|
4374
4779
|
if (m === "GET" || m === "POST" || m === "PATCH" || m === "PUT" || m === "DELETE") {
|
|
4375
4780
|
return m;
|
|
@@ -4385,7 +4790,7 @@ function createMomentumHandler(config) {
|
|
|
4385
4790
|
const pathSegments = (params["momentum"] ?? "").split("/").filter(Boolean);
|
|
4386
4791
|
const collectionSlug = pathSegments[0] ?? "";
|
|
4387
4792
|
const id = pathSegments[1];
|
|
4388
|
-
const queryParams = getQuery(event);
|
|
4793
|
+
const queryParams = nestBracketParams(getQuery(event));
|
|
4389
4794
|
const sortParam = queryParams["sort"];
|
|
4390
4795
|
const query = {
|
|
4391
4796
|
limit: queryParams["limit"] ? Number(queryParams["limit"]) : void 0,
|
|
@@ -4518,7 +4923,7 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4518
4923
|
const user = context?.user;
|
|
4519
4924
|
const params = utils.getRouterParams(event);
|
|
4520
4925
|
const pathSegments = (params["momentum"] ?? "").split("/").filter(Boolean);
|
|
4521
|
-
const queryParams = utils.getQuery(event);
|
|
4926
|
+
const queryParams = nestBracketParams(utils.getQuery(event));
|
|
4522
4927
|
const seg0 = pathSegments[0] ?? "";
|
|
4523
4928
|
const seg1 = pathSegments[1];
|
|
4524
4929
|
const seg2 = pathSegments[2];
|
|
@@ -4761,7 +5166,7 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4761
5166
|
utils.setResponseStatus(event, 400);
|
|
4762
5167
|
return { error: "File path required" };
|
|
4763
5168
|
}
|
|
4764
|
-
const { normalize, isAbsolute, resolve, sep } = await import("node:path");
|
|
5169
|
+
const { normalize: normalize2, isAbsolute, resolve: resolve2, sep } = await import("node:path");
|
|
4765
5170
|
let decodedPath;
|
|
4766
5171
|
try {
|
|
4767
5172
|
decodedPath = decodeURIComponent(rawPath);
|
|
@@ -4769,13 +5174,17 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
4769
5174
|
utils.setResponseStatus(event, 400);
|
|
4770
5175
|
return { error: "Invalid path encoding" };
|
|
4771
5176
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
5177
|
+
if (decodedPath.includes("..")) {
|
|
5178
|
+
utils.setResponseStatus(event, 403);
|
|
5179
|
+
return { error: "Invalid file path" };
|
|
5180
|
+
}
|
|
5181
|
+
const filePath = normalize2(decodedPath);
|
|
5182
|
+
if (isAbsolute(filePath)) {
|
|
4774
5183
|
utils.setResponseStatus(event, 403);
|
|
4775
5184
|
return { error: "Invalid file path" };
|
|
4776
5185
|
}
|
|
4777
|
-
const fakeRoot =
|
|
4778
|
-
const resolved =
|
|
5186
|
+
const fakeRoot = resolve2("/safe-root");
|
|
5187
|
+
const resolved = resolve2(fakeRoot, filePath);
|
|
4779
5188
|
if (!resolved.startsWith(fakeRoot + sep) && resolved !== fakeRoot) {
|
|
4780
5189
|
utils.setResponseStatus(event, 403);
|
|
4781
5190
|
return { error: "Invalid file path" };
|
|
@@ -5272,6 +5681,140 @@ function createComprehensiveMomentumHandler(config) {
|
|
|
5272
5681
|
return { error: sanitizeErrorMessage(error, "Import failed") };
|
|
5273
5682
|
}
|
|
5274
5683
|
}
|
|
5684
|
+
const postUploadCol = seg0 ? config.collections.find((c) => c.slug === seg0) : void 0;
|
|
5685
|
+
if (method === "POST" && seg0 && !seg1 && postUploadCol && isUploadCollection(postUploadCol)) {
|
|
5686
|
+
if (!user) {
|
|
5687
|
+
utils.setResponseStatus(event, 401);
|
|
5688
|
+
return { error: "Authentication required to upload files" };
|
|
5689
|
+
}
|
|
5690
|
+
const uploadConfig = getUploadConfig(config);
|
|
5691
|
+
if (!uploadConfig) {
|
|
5692
|
+
utils.setResponseStatus(event, 500);
|
|
5693
|
+
return { error: "Storage not configured" };
|
|
5694
|
+
}
|
|
5695
|
+
const formData = await utils.readMultipartFormData(event);
|
|
5696
|
+
if (!formData || formData.length === 0) {
|
|
5697
|
+
utils.setResponseStatus(event, 400);
|
|
5698
|
+
return { error: "No file provided" };
|
|
5699
|
+
}
|
|
5700
|
+
const fileField = formData.find((f) => f.name === "file");
|
|
5701
|
+
if (!fileField || !fileField.filename) {
|
|
5702
|
+
utils.setResponseStatus(event, 400);
|
|
5703
|
+
return { error: "No file provided" };
|
|
5704
|
+
}
|
|
5705
|
+
const file = {
|
|
5706
|
+
originalName: fileField.filename,
|
|
5707
|
+
mimeType: fileField.type ?? "application/octet-stream",
|
|
5708
|
+
size: fileField.data.length,
|
|
5709
|
+
buffer: fileField.data
|
|
5710
|
+
};
|
|
5711
|
+
const fields = {};
|
|
5712
|
+
for (const field of formData) {
|
|
5713
|
+
if (field.name !== "file" && field.name) {
|
|
5714
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
const uploadRequest = {
|
|
5718
|
+
file,
|
|
5719
|
+
user,
|
|
5720
|
+
fields,
|
|
5721
|
+
collectionSlug: seg0,
|
|
5722
|
+
collectionUpload: postUploadCol.upload
|
|
5723
|
+
};
|
|
5724
|
+
const response2 = await handleCollectionUpload(uploadConfig, uploadRequest);
|
|
5725
|
+
utils.setResponseStatus(event, response2.status);
|
|
5726
|
+
return response2;
|
|
5727
|
+
}
|
|
5728
|
+
const patchUploadCol = seg0 ? config.collections.find((c) => c.slug === seg0) : void 0;
|
|
5729
|
+
if (method === "PATCH" && seg0 && seg1 && patchUploadCol && isUploadCollection(patchUploadCol)) {
|
|
5730
|
+
if (!user) {
|
|
5731
|
+
utils.setResponseStatus(event, 401);
|
|
5732
|
+
return { error: "Authentication required to upload files" };
|
|
5733
|
+
}
|
|
5734
|
+
const formData = await utils.readMultipartFormData(event);
|
|
5735
|
+
if (formData) {
|
|
5736
|
+
const uploadConfig = getUploadConfig(config);
|
|
5737
|
+
if (!uploadConfig) {
|
|
5738
|
+
utils.setResponseStatus(event, 500);
|
|
5739
|
+
return { error: "Storage not configured" };
|
|
5740
|
+
}
|
|
5741
|
+
const fileField = formData.find((f) => f.name === "file");
|
|
5742
|
+
if (fileField?.filename) {
|
|
5743
|
+
const file = {
|
|
5744
|
+
originalName: fileField.filename,
|
|
5745
|
+
mimeType: fileField.type ?? "application/octet-stream",
|
|
5746
|
+
size: fileField.data.length,
|
|
5747
|
+
buffer: fileField.data
|
|
5748
|
+
};
|
|
5749
|
+
const maxFileSize = patchUploadCol.upload?.maxFileSize ?? uploadConfig.maxFileSize ?? 10 * 1024 * 1024;
|
|
5750
|
+
const allowedMimeTypes = patchUploadCol.upload?.mimeTypes ?? uploadConfig.allowedMimeTypes ?? [];
|
|
5751
|
+
if (file.size > maxFileSize) {
|
|
5752
|
+
const maxMB = (maxFileSize / (1024 * 1024)).toFixed(1);
|
|
5753
|
+
utils.setResponseStatus(event, 400);
|
|
5754
|
+
return { error: `File too large. Maximum size is ${maxMB}MB` };
|
|
5755
|
+
}
|
|
5756
|
+
const mimeError = validateMimeType2(file.mimeType, allowedMimeTypes);
|
|
5757
|
+
if (mimeError) {
|
|
5758
|
+
utils.setResponseStatus(event, 400);
|
|
5759
|
+
return { error: mimeError };
|
|
5760
|
+
}
|
|
5761
|
+
if (file.buffer && file.buffer.length > 0) {
|
|
5762
|
+
const { validateMimeType: validateMimeByMagicBytes } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
5763
|
+
const magicByteResult = validateMimeByMagicBytes(
|
|
5764
|
+
file.buffer,
|
|
5765
|
+
file.mimeType,
|
|
5766
|
+
allowedMimeTypes
|
|
5767
|
+
);
|
|
5768
|
+
if (!magicByteResult.valid) {
|
|
5769
|
+
utils.setResponseStatus(event, 400);
|
|
5770
|
+
return {
|
|
5771
|
+
error: magicByteResult.error ?? "File content does not match claimed type"
|
|
5772
|
+
};
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
const storedFile = await uploadConfig.adapter.upload(file);
|
|
5776
|
+
const fields = {};
|
|
5777
|
+
for (const field of formData ?? []) {
|
|
5778
|
+
if (field.name !== "file" && field.name) {
|
|
5779
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
const updateData = {
|
|
5783
|
+
...fields,
|
|
5784
|
+
filename: file.originalName,
|
|
5785
|
+
mimeType: file.mimeType,
|
|
5786
|
+
filesize: file.size,
|
|
5787
|
+
path: storedFile.path,
|
|
5788
|
+
url: storedFile.url
|
|
5789
|
+
};
|
|
5790
|
+
try {
|
|
5791
|
+
const api = getMomentumAPI().setContext({ user });
|
|
5792
|
+
const doc = await api.collection(seg0).update(seg1, updateData);
|
|
5793
|
+
return { doc };
|
|
5794
|
+
} catch (error) {
|
|
5795
|
+
utils.setResponseStatus(event, 500);
|
|
5796
|
+
return { error: sanitizeErrorMessage(error, "Failed to update document") };
|
|
5797
|
+
}
|
|
5798
|
+
} else {
|
|
5799
|
+
const fields = {};
|
|
5800
|
+
for (const field of formData ?? []) {
|
|
5801
|
+
if (field.name) {
|
|
5802
|
+
fields[field.name] = field.data.toString("utf-8");
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
const request2 = {
|
|
5806
|
+
method: "PATCH",
|
|
5807
|
+
collectionSlug: seg0,
|
|
5808
|
+
id: seg1,
|
|
5809
|
+
body: fields,
|
|
5810
|
+
user
|
|
5811
|
+
};
|
|
5812
|
+
const response2 = await handlers.routeRequest(request2);
|
|
5813
|
+
utils.setResponseStatus(event, response2.status ?? 200);
|
|
5814
|
+
return response2;
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5275
5818
|
const collectionSlug = seg0;
|
|
5276
5819
|
const id = seg1;
|
|
5277
5820
|
const sortParam = queryParams["sort"];
|