@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.
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.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/server-analog/src/index.ts
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(src_exports);
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
- const filePath = normalize(decodedPath).replace(/^(\.\.(\/|\\|$))+/, "");
4773
- if (isAbsolute(filePath) || filePath.includes("..") || filePath.includes(`${sep}..`)) {
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 = resolve("/safe-root");
4778
- const resolved = resolve(fakeRoot, filePath);
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"];