@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.
Files changed (4) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/index.cjs +755 -212
  3. package/index.js +759 -210
  4. 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
- const filePath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, "");
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 fakeRoot = resolve("/safe-root");
4753
- const resolved = resolve(fakeRoot, filePath);
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"];