lieko-express 0.0.3 → 0.0.5

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/lieko-express.js CHANGED
@@ -422,6 +422,127 @@ class LiekoExpress {
422
422
  strictTrailingSlash: true,
423
423
  allowTrailingSlash: false,
424
424
  };
425
+
426
+ this.bodyParserOptions = {
427
+ json: {
428
+ limit: '10mb',
429
+ strict: true
430
+ },
431
+ urlencoded: {
432
+ limit: '10mb',
433
+ extended: true
434
+ },
435
+ multipart: {
436
+ limit: '10mb'
437
+ }
438
+ };
439
+
440
+ this.corsOptions = {
441
+ enabled: false,
442
+ origin: "*",
443
+ strictOrigin: false,
444
+ allowPrivateNetwork: false,
445
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
446
+ headers: ["Content-Type", "Authorization"],
447
+ credentials: false,
448
+ maxAge: 86400,
449
+ exposedHeaders: [],
450
+ debug: false
451
+ };
452
+ }
453
+
454
+ cors(options = {}) {
455
+ this.corsOptions = {
456
+ ...this.corsOptions,
457
+ enabled: true,
458
+ ...options
459
+ };
460
+ }
461
+
462
+ _matchOrigin(origin, allowedOrigin) {
463
+ if (!origin || !allowedOrigin) return false;
464
+
465
+ if (Array.isArray(allowedOrigin)) {
466
+ return allowedOrigin.some(o => this._matchOrigin(origin, o));
467
+ }
468
+
469
+ if (allowedOrigin === "*") return true;
470
+
471
+ // Wildcard https://*.example.com
472
+ if (allowedOrigin.includes("*")) {
473
+ const regex = new RegExp("^" + allowedOrigin
474
+ .replace(/\./g, "\\.")
475
+ .replace(/\*/g, ".*") + "$");
476
+
477
+ return regex.test(origin);
478
+ }
479
+
480
+ return origin === allowedOrigin;
481
+ }
482
+
483
+ _applyCors(req, res, opts) {
484
+ if (!opts || !opts.enabled) return;
485
+
486
+ const requestOrigin = req.headers.origin || "";
487
+
488
+ let finalOrigin = "*";
489
+
490
+ if (opts.strictOrigin && requestOrigin) {
491
+ const allowed = this._matchOrigin(requestOrigin, opts.origin);
492
+
493
+ if (!allowed) {
494
+ res.statusCode = 403;
495
+ return res.end(JSON.stringify({
496
+ success: false,
497
+ error: "Origin Forbidden",
498
+ message: `Origin "${requestOrigin}" is not allowed`
499
+ }));
500
+ }
501
+ }
502
+
503
+ if (opts.origin === "*") {
504
+ finalOrigin = "*";
505
+ } else if (Array.isArray(opts.origin)) {
506
+ const match = opts.origin.find(o => this._matchOrigin(requestOrigin, o));
507
+ finalOrigin = match || opts.origin[0];
508
+ } else {
509
+ finalOrigin = this._matchOrigin(requestOrigin, opts.origin)
510
+ ? requestOrigin
511
+ : opts.origin;
512
+ }
513
+
514
+ this._logCorsDebug(req, {
515
+ ...opts,
516
+ origin: finalOrigin
517
+ });
518
+
519
+ res.setHeader("Access-Control-Allow-Origin", finalOrigin);
520
+
521
+ if (opts.credentials) {
522
+ res.setHeader("Access-Control-Allow-Credentials", "true");
523
+ }
524
+
525
+ if (opts.exposedHeaders?.length) {
526
+ res.setHeader("Access-Control-Expose-Headers",
527
+ opts.exposedHeaders.join(", "));
528
+ }
529
+
530
+ // Chrome Private Network Access
531
+ if (
532
+ opts.allowPrivateNetwork &&
533
+ req.headers["access-control-request-private-network"] === "true"
534
+ ) {
535
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
536
+ }
537
+
538
+ if (req.method === "OPTIONS") {
539
+ res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
540
+ res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
541
+ res.setHeader("Access-Control-Max-Age", opts.maxAge);
542
+
543
+ res.statusCode = 204;
544
+ return res.end();
545
+ }
425
546
  }
426
547
 
427
548
  set(name, value) {
@@ -451,6 +572,244 @@ class LiekoExpress {
451
572
  return !this.settings[name];
452
573
  }
453
574
 
575
+ bodyParser(options = {}) {
576
+ if (options.limit) {
577
+ this.bodyParserOptions.json.limit = options.limit;
578
+ this.bodyParserOptions.urlencoded.limit = options.limit;
579
+ }
580
+ if (options.extended !== undefined) {
581
+ this.bodyParserOptions.urlencoded.extended = options.extended;
582
+ }
583
+ if (options.strict !== undefined) {
584
+ this.bodyParserOptions.json.strict = options.strict;
585
+ }
586
+ return this;
587
+ }
588
+
589
+ json(options = {}) {
590
+ if (options.limit) {
591
+ this.bodyParserOptions.json.limit = options.limit;
592
+ }
593
+ if (options.strict !== undefined) {
594
+ this.bodyParserOptions.json.strict = options.strict;
595
+ }
596
+ return this;
597
+ }
598
+
599
+ urlencoded(options = {}) {
600
+ if (options.limit) {
601
+ this.bodyParserOptions.urlencoded.limit = options.limit;
602
+ }
603
+ if (options.extended !== undefined) {
604
+ this.bodyParserOptions.urlencoded.extended = options.extended;
605
+ }
606
+ return this;
607
+ }
608
+
609
+ multipart(options = {}) {
610
+ if (options.limit) {
611
+ this.bodyParserOptions.multipart.limit = options.limit;
612
+ }
613
+ return this;
614
+ }
615
+
616
+ _parseLimit(limit) {
617
+ if (typeof limit === 'number') return limit;
618
+
619
+ const match = limit.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
620
+ if (!match) return 1048576; // 1mb par dƩfaut
621
+
622
+ const value = parseFloat(match[1]);
623
+ const unit = (match[2] || 'b').toLowerCase();
624
+
625
+ const multipliers = {
626
+ b: 1,
627
+ kb: 1024,
628
+ mb: 1024 * 1024,
629
+ gb: 1024 * 1024 * 1024
630
+ };
631
+
632
+ return value * multipliers[unit];
633
+ }
634
+
635
+ async _parseBody(req, routeOptions = null) {
636
+ return new Promise((resolve, reject) => {
637
+
638
+ if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
639
+ req.body = {};
640
+ req.files = {};
641
+ req._bodySize = 0;
642
+ return resolve();
643
+ }
644
+
645
+ const contentType = (req.headers['content-type'] || '').toLowerCase();
646
+ const options = routeOptions || this.bodyParserOptions;
647
+
648
+ req.body = {};
649
+ req.files = {};
650
+
651
+ let raw = Buffer.alloc(0);
652
+ let size = 0;
653
+ let limitExceeded = false;
654
+ let errorSent = false;
655
+
656
+ const detectLimit = () => {
657
+ if (contentType.includes('application/json')) {
658
+ return this._parseLimit(options.json.limit);
659
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
660
+ return this._parseLimit(options.urlencoded.limit);
661
+ } else if (contentType.includes('multipart/form-data')) {
662
+ return this._parseLimit(options.multipart.limit);
663
+ } else {
664
+ return this._parseLimit('1mb');
665
+ }
666
+ };
667
+
668
+ const limit = detectLimit();
669
+ const limitLabel =
670
+ contentType.includes('application/json') ? options.json.limit :
671
+ contentType.includes('application/x-www-form-urlencoded') ? options.urlencoded.limit :
672
+ contentType.includes('multipart/form-data') ? options.multipart.limit :
673
+ '1mb';
674
+
675
+ req.on('data', chunk => {
676
+ if (limitExceeded || errorSent) return;
677
+
678
+ size += chunk.length;
679
+
680
+ if (size > limit) {
681
+ limitExceeded = true;
682
+ errorSent = true;
683
+
684
+ req.removeAllListeners('data');
685
+ req.removeAllListeners('end');
686
+ req.removeAllListeners('error');
687
+
688
+ req.on('data', () => { });
689
+ req.on('end', () => { });
690
+
691
+ const error = new Error(`Request body too large. Limit: ${limitLabel}`);
692
+ error.status = 413;
693
+ error.code = 'PAYLOAD_TOO_LARGE';
694
+ return reject(error);
695
+ }
696
+
697
+ raw = Buffer.concat([raw, chunk]);
698
+ });
699
+
700
+ req.on('end', () => {
701
+ if (limitExceeded) return;
702
+
703
+ req._bodySize = size;
704
+
705
+ try {
706
+
707
+ if (contentType.includes('application/json')) {
708
+ const text = raw.toString();
709
+ try {
710
+ req.body = JSON.parse(text);
711
+
712
+ if (options.json.strict && text.trim() && !['[', '{'].includes(text.trim()[0])) {
713
+ return reject(new Error('Strict mode: body must be an object or array'));
714
+ }
715
+ } catch (err) {
716
+ req.body = {};
717
+ }
718
+ }
719
+
720
+ else if (contentType.includes('application/x-www-form-urlencoded')) {
721
+ const text = raw.toString();
722
+ const params = new URLSearchParams(text);
723
+ req.body = {};
724
+
725
+ if (options.urlencoded.extended) {
726
+ for (const [key, value] of params) {
727
+ if (key.includes('[')) {
728
+ const match = key.match(/^([^\[]+)\[([^\]]*)\]$/);
729
+ if (match) {
730
+ const [, objKey, subKey] = match;
731
+ if (!req.body[objKey]) req.body[objKey] = {};
732
+ if (subKey) req.body[objKey][subKey] = value;
733
+ else {
734
+ if (!Array.isArray(req.body[objKey])) req.body[objKey] = [];
735
+ req.body[objKey].push(value);
736
+ }
737
+ continue;
738
+ }
739
+ }
740
+ req.body[key] = value;
741
+ }
742
+ } else {
743
+ req.body = Object.fromEntries(params);
744
+ }
745
+ }
746
+
747
+ else if (contentType.includes('multipart/form-data')) {
748
+ const boundaryMatch = contentType.match(/boundary=([^;]+)/);
749
+ if (!boundaryMatch) return reject(new Error('Missing multipart boundary'));
750
+
751
+ const boundary = '--' + boundaryMatch[1];
752
+
753
+ const text = raw.toString('binary');
754
+ const parts = text.split(boundary).filter(p => p && !p.includes('--'));
755
+
756
+ for (let part of parts) {
757
+ const headerEnd = part.indexOf('\r\n\r\n');
758
+ if (headerEnd === -1) continue;
759
+
760
+ const headers = part.slice(0, headerEnd);
761
+ const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
762
+
763
+ const nameMatch = headers.match(/name="([^"]+)"/);
764
+ const filenameMatch = headers.match(/filename="([^"]*)"/);
765
+ const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i);
766
+
767
+ const field = nameMatch?.[1];
768
+ if (!field) continue;
769
+
770
+ if (filenameMatch?.[1]) {
771
+ const bin = Buffer.from(body, 'binary');
772
+
773
+ req.files[field] = {
774
+ filename: filenameMatch[1],
775
+ data: bin,
776
+ size: bin.length,
777
+ contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream'
778
+ };
779
+ } else {
780
+ req.body[field] = body;
781
+ }
782
+ }
783
+ }
784
+
785
+ else {
786
+ const text = raw.toString();
787
+ req.body = text ? { text } : {};
788
+ }
789
+
790
+ for (const key in req.body) {
791
+ const value = req.body[key];
792
+
793
+ if (typeof value === 'string' && value.trim() !== '' && !isNaN(value)) {
794
+ req.body[key] = parseFloat(value);
795
+ } else if (value === 'true') {
796
+ req.body[key] = true;
797
+ } else if (value === 'false') {
798
+ req.body[key] = false;
799
+ }
800
+ }
801
+
802
+ resolve();
803
+
804
+ } catch (error) {
805
+ reject(error);
806
+ }
807
+ });
808
+
809
+ req.on('error', reject);
810
+ });
811
+ }
812
+
454
813
  get(path, ...handlers) {
455
814
  this._addRoute('GET', path, ...handlers);
456
815
  return this;
@@ -540,24 +899,21 @@ class LiekoExpress {
540
899
  }
541
900
 
542
901
  _checkMiddleware(handler) {
543
- const isAsync = handler.constructor.name === 'AsyncFunction';
902
+ const isAsync = handler instanceof (async () => { }).constructor;
544
903
 
545
- if (isAsync) {
546
- return;
547
- }
548
-
549
- const paramCount = handler.length;
904
+ if (isAsync) return;
550
905
 
551
- if (paramCount < 3) {
906
+ if (handler.length < 3) {
552
907
  console.warn(`
553
- āš ļø WARNING: Synchronous middleware detected without 'next' parameter!
554
- This may cause the request to hang indefinitely.
555
-
556
- Your middleware: ${handler.toString().split('\n')[0].substring(0, 80)}...
557
-
558
- Fix: Add 'next' as third parameter and call it:
559
- āœ… (req, res, next) => { /* your code */ next(); }
560
- `);
908
+ āš ļø WARNING: Middleware executed without a 'next' parameter.
909
+ This middleware may block the request pipeline.
910
+
911
+ Offending middleware:
912
+ ${handler.toString().split('\n')[0].substring(0, 120)}...
913
+
914
+ Fix: Add 'next' as third parameter and call it:
915
+ (req, res, next) => { /* your code */ next(); }
916
+ `);
561
917
  }
562
918
  }
563
919
 
@@ -655,7 +1011,8 @@ class LiekoExpress {
655
1011
  groupChain: [
656
1012
  ...this.groupStack,
657
1013
  ...(route.groupChain || [])
658
- ]
1014
+ ],
1015
+ bodyParserOptions: router.bodyParserOptions
659
1016
  });
660
1017
  });
661
1018
  }
@@ -772,7 +1129,7 @@ class LiekoExpress {
772
1129
  }
773
1130
 
774
1131
  const HTTP_STATUS = {
775
- // 4xx — CLIENT ERRORS
1132
+ // 4xx – CLIENT ERRORS
776
1133
  INVALID_REQUEST: 400,
777
1134
  VALIDATION_FAILED: 400,
778
1135
  NO_TOKEN_PROVIDED: 401,
@@ -784,7 +1141,7 @@ class LiekoExpress {
784
1141
  RECORD_EXISTS: 409,
785
1142
  TOO_MANY_REQUESTS: 429,
786
1143
 
787
- // 5xx — SERVER ERRORS
1144
+ // 5xx – SERVER ERRORS
788
1145
  SERVER_ERROR: 500,
789
1146
  SERVICE_UNAVAILABLE: 503
790
1147
  };
@@ -848,43 +1205,89 @@ class LiekoExpress {
848
1205
 
849
1206
  async _handleRequest(req, res) {
850
1207
  this._enhanceRequest(req);
1208
+
851
1209
  const url = req.url;
852
- const questionMarkIndex = url.indexOf('?');
853
- const pathname = questionMarkIndex === -1 ? url : url.substring(0, questionMarkIndex);
1210
+ const qIndex = url.indexOf('?');
1211
+ const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
854
1212
 
855
1213
  const query = {};
856
- if (questionMarkIndex !== -1) {
857
- const searchParams = new URLSearchParams(url.substring(questionMarkIndex + 1));
1214
+ if (qIndex !== -1) {
1215
+ const searchParams = new URLSearchParams(url.substring(qIndex + 1));
858
1216
  for (const [key, value] of searchParams) query[key] = value;
859
1217
  }
860
-
861
1218
  req.query = query;
862
1219
  req.params = {};
863
1220
 
864
1221
  for (const key in req.query) {
865
- const value = req.query[key];
866
- if (value === 'true') req.query[key] = true;
867
- if (value === 'false') req.query[key] = false;
868
- if (/^\d+$/.test(value)) req.query[key] = parseInt(value);
869
- if (/^\d+\.\d+$/.test(value)) req.query[key] = parseFloat(value);
1222
+ const v = req.query[key];
1223
+ if (v === 'true') req.query[key] = true;
1224
+ else if (v === 'false') req.query[key] = false;
1225
+ else if (/^\d+$/.test(v)) req.query[key] = parseInt(v);
1226
+ else if (/^\d+\.\d+$/.test(v)) req.query[key] = parseFloat(v);
870
1227
  }
871
1228
 
872
- await this._parseBody(req);
873
1229
  req._startTime = process.hrtime.bigint();
874
1230
  this._enhanceResponse(req, res);
875
1231
 
876
1232
  try {
1233
+
1234
+ if (req.method === "OPTIONS" && this.corsOptions.enabled) {
1235
+ this._applyCors(req, res, this.corsOptions);
1236
+ return;
1237
+ }
1238
+
1239
+ const route = this._findRoute(req.method, pathname);
1240
+
1241
+ if (route) {
1242
+
1243
+ if (route.cors === false) { }
1244
+
1245
+ else if (route.cors) {
1246
+ const finalCors = {
1247
+ ...this.corsOptions,
1248
+ enabled: true,
1249
+ ...route.cors
1250
+ };
1251
+
1252
+ this._applyCors(req, res, finalCors);
1253
+ if (req.method === "OPTIONS") return;
1254
+ }
1255
+
1256
+ else if (this.corsOptions.enabled) {
1257
+ this._applyCors(req, res, this.corsOptions);
1258
+ if (req.method === "OPTIONS") return;
1259
+ }
1260
+
1261
+ } else {
1262
+
1263
+ if (this.corsOptions.enabled) {
1264
+ this._applyCors(req, res, this.corsOptions);
1265
+ if (req.method === "OPTIONS") return;
1266
+ }
1267
+ }
1268
+
1269
+ try {
1270
+ await this._parseBody(req, route ? route.bodyParserOptions : null);
1271
+ } catch (error) {
1272
+ if (error.code === 'PAYLOAD_TOO_LARGE') {
1273
+ return res.status(413).json({
1274
+ success: false,
1275
+ error: 'Payload Too Large',
1276
+ message: error.message
1277
+ });
1278
+ }
1279
+ return await this._runErrorHandlers(error, req, res);
1280
+ }
1281
+
877
1282
  for (const mw of this.middlewares) {
878
1283
  if (res.headersSent) return;
879
1284
 
880
- if (mw.path && !pathname.startsWith(mw.path)) {
881
- continue;
882
- }
1285
+ if (mw.path && !pathname.startsWith(mw.path)) continue;
883
1286
 
884
1287
  await new Promise((resolve, reject) => {
885
- const next = async (error) => {
886
- if (error) {
887
- await this._runErrorHandlers(error, req, res);
1288
+ const next = async (err) => {
1289
+ if (err) {
1290
+ await this._runErrorHandlers(err, req, res);
888
1291
  return resolve();
889
1292
  }
890
1293
  resolve();
@@ -899,14 +1302,9 @@ class LiekoExpress {
899
1302
 
900
1303
  if (res.headersSent) return;
901
1304
 
902
- const route = this._findRoute(req.method, pathname);
903
-
904
1305
  if (!route) {
905
- if (this.notFoundHandler) {
906
- return this.notFoundHandler(req, res);
907
- } else {
908
- return res.status(404).json({ error: 'Route not found' });
909
- }
1306
+ if (this.notFoundHandler) return this.notFoundHandler(req, res);
1307
+ return res.status(404).json({ error: 'Route not found' });
910
1308
  }
911
1309
 
912
1310
  req.params = route.params;
@@ -915,9 +1313,9 @@ class LiekoExpress {
915
1313
  if (res.headersSent) return;
916
1314
 
917
1315
  await new Promise((resolve, reject) => {
918
- const next = async (error) => {
919
- if (error) {
920
- await this._runErrorHandlers(error, req, res);
1316
+ const next = async (err) => {
1317
+ if (err) {
1318
+ await this._runErrorHandlers(err, req, res);
921
1319
  return resolve();
922
1320
  }
923
1321
  resolve();
@@ -1175,85 +1573,6 @@ class LiekoExpress {
1175
1573
  };
1176
1574
  }
1177
1575
 
1178
- async _parseBody(req) {
1179
- return new Promise((resolve) => {
1180
- if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
1181
- req.body = {};
1182
- req.files = {};
1183
- return resolve();
1184
- }
1185
-
1186
- const contentType = (req.headers['content-type'] || '').toLowerCase();
1187
- req.body = {};
1188
- req.files = {};
1189
-
1190
- let raw = '';
1191
- req.on('data', chunk => raw += chunk);
1192
- req.on('end', () => {
1193
-
1194
- if (contentType.includes('application/json')) {
1195
- try {
1196
- req.body = JSON.parse(raw);
1197
- } catch {
1198
- req.body = {};
1199
- }
1200
- }
1201
- else if (contentType.includes('application/x-www-form-urlencoded')) {
1202
- req.body = Object.fromEntries(new URLSearchParams(raw));
1203
- }
1204
- else if (contentType.includes('multipart/form-data')) {
1205
- const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
1206
- if (boundaryMatch) {
1207
- const boundary = '--' + boundaryMatch[1];
1208
- const parts = raw.split(boundary).filter(p => p && !p.includes('--'));
1209
-
1210
- for (let part of parts) {
1211
- const headerEnd = part.indexOf('\r\n\r\n');
1212
- if (headerEnd === -1) continue;
1213
-
1214
- const headers = part.slice(0, headerEnd);
1215
- const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
1216
-
1217
- const nameMatch = headers.match(/name="([^"]+)"/);
1218
- const filenameMatch = headers.match(/filename="([^"]*)"/);
1219
- const field = nameMatch?.[1];
1220
- if (!field) continue;
1221
-
1222
- if (filenameMatch?.[1]) {
1223
- req.files[field] = {
1224
- filename: filenameMatch[1],
1225
- data: Buffer.from(body, 'binary'),
1226
- contentType: headers.match(/Content-Type: (.*)/i)?.[1] || 'application/octet-stream'
1227
- };
1228
- } else {
1229
- req.body[field] = body;
1230
- }
1231
- }
1232
- }
1233
- }
1234
- else {
1235
- req.body = raw ? { text: raw } : {};
1236
- }
1237
-
1238
- for (const key in req.body) {
1239
- const value = req.body[key];
1240
-
1241
- if (typeof value === 'string' && value.trim() !== '' && !isNaN(value) && !isNaN(parseFloat(value))) {
1242
- req.body[key] = parseFloat(value);
1243
- }
1244
- else if (value === 'true') {
1245
- req.body[key] = true;
1246
- }
1247
- else if (value === 'false') {
1248
- req.body[key] = false;
1249
- }
1250
- }
1251
-
1252
- resolve();
1253
- });
1254
- });
1255
- }
1256
-
1257
1576
  async _runMiddleware(handler, req, res) {
1258
1577
  return new Promise((resolve, reject) => {
1259
1578
  const next = (err) => err ? reject(err) : resolve();
@@ -1286,20 +1605,63 @@ class LiekoExpress {
1286
1605
  code >= 300 ? '\x1b[36m' :
1287
1606
  '\x1b[32m';
1288
1607
 
1608
+ const bodySize = req._bodySize || 0;
1609
+ let bodySizeFormatted;
1610
+
1611
+ if (bodySize === 0) {
1612
+ bodySizeFormatted = '0 bytes';
1613
+ } else if (bodySize < 1024) {
1614
+ bodySizeFormatted = `${bodySize} bytes`;
1615
+ } else if (bodySize < 1024 * 1024) {
1616
+ bodySizeFormatted = `${(bodySize / 1024).toFixed(2)} KB`;
1617
+ } else if (bodySize < 1024 * 1024 * 1024) {
1618
+ bodySizeFormatted = `${(bodySize / (1024 * 1024)).toFixed(2)} MB`;
1619
+ } else {
1620
+ bodySizeFormatted = `${(bodySize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
1621
+ }
1622
+
1289
1623
  console.log(
1290
1624
  `\n🟦 DEBUG REQUEST` +
1291
1625
  `\n→ ${req.method} ${req.originalUrl}` +
1292
1626
  `\n→ IP: ${req.ip.ipv4}` +
1293
1627
  `\n→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m` +
1294
1628
  `\n→ Duration: ${timeFormatted}` +
1629
+ `\n→ Body Size: ${bodySizeFormatted}` +
1295
1630
  `\n→ Params: ${JSON.stringify(req.params || {})}` +
1296
1631
  `\n→ Query: ${JSON.stringify(req.query || {})}` +
1297
- `\n→ Body: ${JSON.stringify(req.body || {})}` +
1632
+ `\n→ Body: ${JSON.stringify(req.body || {}).substring(0, 200)}${JSON.stringify(req.body || {}).length > 200 ? '...' : ''}` +
1298
1633
  `\n→ Files: ${Object.keys(req.files || {}).join(', ')}` +
1299
1634
  `\n---------------------------------------------\n`
1300
1635
  );
1301
1636
  }
1302
1637
 
1638
+ _logCorsDebug(req, opts) {
1639
+ if (!opts.debug) return;
1640
+
1641
+ console.log("\n[CORS DEBUG]");
1642
+ console.log("Request:", req.method, req.url);
1643
+ console.log("Origin:", req.headers.origin || "none");
1644
+
1645
+ console.log("Applied CORS Policy:");
1646
+ console.log(" - Access-Control-Allow-Origin:", opts.origin);
1647
+ console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
1648
+ console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
1649
+
1650
+ if (opts.credentials) {
1651
+ console.log(" - Access-Control-Allow-Credentials: true");
1652
+ }
1653
+
1654
+ if (opts.exposedHeaders?.length) {
1655
+ console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
1656
+ }
1657
+
1658
+ console.log(" - Max-Age:", opts.maxAge);
1659
+
1660
+ if (req.method === "OPTIONS") {
1661
+ console.log("Preflight request handled with status 204\n");
1662
+ }
1663
+ }
1664
+
1303
1665
  _buildRouteTree() {
1304
1666
  const tree = {};
1305
1667
 
@@ -1403,4 +1765,4 @@ module.exports.schema = (...args) => new Schema(...args);
1403
1765
  module.exports.validators = validators;
1404
1766
  module.exports.validate = validate;
1405
1767
  module.exports.validatePartial = validatePartial;
1406
- module.exports.ValidationError = ValidationError;
1768
+ module.exports.ValidationError = ValidationError;