lieko-express 0.0.3 → 0.0.4

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 (3) hide show
  1. package/README.md +458 -18
  2. package/lieko-express.js +488 -126
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -311,7 +311,7 @@ app.use((req, res, next) => {
311
311
  next();
312
312
  });
313
313
 
314
- // CORS middleware
314
+ // CORS middleware (native, but you can use app.cors())
315
315
  app.use((req, res, next) => {
316
316
  res.set('Access-Control-Allow-Origin', '*');
317
317
  res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
@@ -382,6 +382,380 @@ app.use(async (req, res, next) => {
382
382
  });
383
383
  ```
384
384
 
385
+ # **CORS**
386
+
387
+
388
+ Lieko-Express includes a **fully built-in, high-performance CORS engine**, with:
389
+
390
+ * Global CORS (`app.cors()`)
391
+ * Route-level CORS (`app.get("/test", { cors: {...} }, handler)`—coming soon if you want it)
392
+ * Wildcard origins (`https://*.example.com`)
393
+ * Multiple allowed origins
394
+ * Strict mode (reject unknown origins)
395
+ * Credential support
396
+ * Private Network Access (Chrome PNA)
397
+ * Debug logging
398
+ * Full OPTIONS preflight support
399
+
400
+ ---
401
+
402
+ # **Enable CORS Globally**
403
+
404
+ ```js
405
+ app.cors();
406
+ ```
407
+
408
+ This enables default permissive CORS:
409
+
410
+ | Option | Default |
411
+ | --------------------- | ----------------------------- |
412
+ | `origin` | `"*"` |
413
+ | `methods` | all standard methods |
414
+ | `headers` | `Content-Type, Authorization` |
415
+ | `credentials` | false |
416
+ | `maxAge` | 86400 (24h) |
417
+ | `exposedHeaders` | none |
418
+ | `strictOrigin` | false |
419
+ | `allowPrivateNetwork` | false |
420
+
421
+ ---
422
+
423
+ # **Custom Global CORS Options**
424
+
425
+ ```js
426
+ app.cors({
427
+ origin: ["https://example.com", "https://api.example.com"],
428
+ methods: ["GET", "POST"],
429
+ headers: ["Content-Type", "Authorization", "X-Custom"],
430
+ credentials: true,
431
+ maxAge: 3600,
432
+ exposedHeaders: ["X-RateLimit-Remaining"],
433
+ });
434
+ ```
435
+
436
+ ---
437
+
438
+ # **Allow All Origins (Standard Mode)**
439
+
440
+ ```js
441
+ app.cors({
442
+ origin: "*"
443
+ });
444
+ ```
445
+
446
+ ⚠️ If you also set `credentials: true`, browsers will **reject** `*`.
447
+ Lieko will still output `*` because that's the correct spec behavior.
448
+
449
+ ---
450
+
451
+ # **Strict Origin Mode**
452
+
453
+ Strict mode ensures the incoming `Origin` header **must match exactly** one of your allowed origins.
454
+
455
+ ```js
456
+ app.cors({
457
+ origin: ["https://myapp.com"],
458
+ strictOrigin: true
459
+ });
460
+ ```
461
+
462
+ If the request comes from an unauthorized origin:
463
+
464
+ ```json
465
+ HTTP 403
466
+ {
467
+ "success": false,
468
+ "error": "Origin Forbidden",
469
+ "message": "Origin \"https://evil.com\" is not allowed"
470
+ }
471
+ ```
472
+
473
+ ---
474
+
475
+ # **Wildcard Origins**
476
+
477
+ Lieko-Express supports Express-style wildcard patterns:
478
+
479
+ ### Allow any subdomain
480
+
481
+ ```js
482
+ app.cors({
483
+ origin: "https://*.example.com"
484
+ });
485
+ ```
486
+
487
+ Matches:
488
+
489
+ * `https://api.example.com`
490
+ * `https://dashboard.example.com`
491
+
492
+ Does not match:
493
+
494
+ * `http://example.com`
495
+ * `https://example.net`
496
+
497
+ ---
498
+
499
+ # **Allow Private Network Access (Chrome PNA)**
500
+
501
+ ```js
502
+ app.cors({
503
+ allowPrivateNetwork: true
504
+ });
505
+ ```
506
+
507
+ When Chrome sends:
508
+
509
+ ```
510
+ Access-Control-Request-Private-Network: true
511
+ ```
512
+
513
+ Lieko automatically responds:
514
+
515
+ ```
516
+ Access-Control-Allow-Private-Network: true
517
+ ```
518
+
519
+ # Route-Level CORS
520
+
521
+ Lieko-Express allows each route to override or extend the global CORS settings using a `cors` property in the route options.
522
+ This gives you fine-grained control over cross-origin behavior on a per-endpoint basis.
523
+
524
+ Route-level CORS:
525
+
526
+ * Overrides global CORS
527
+ * Supports wildcards (`https://*.example.com`)
528
+ * Supports strict mode
529
+ * Sends its own preflight response
530
+ * Automatically merges with the global configuration
531
+ * Does **not** affect other routes
532
+
533
+ ---
534
+
535
+ ## Basic Example
536
+
537
+ ```js
538
+ app.get("/public", {
539
+ cors: { origin: "*" }
540
+ }, (req, res) => {
541
+ res.ok("Public endpoint with open CORS");
542
+ });
543
+ ```
544
+
545
+ This route allows requests from **any origin**, even if global CORS is configured differently.
546
+
547
+ ---
548
+
549
+ ## 🔒 Restrict a Sensitive Route
550
+
551
+ ```js
552
+ app.post("/admin/login", {
553
+ cors: {
554
+ origin: "https://dashboard.myapp.com",
555
+ credentials: true,
556
+ strictOrigin: true
557
+ }
558
+ }, (req, res) => {
559
+ res.ok("Login OK");
560
+ });
561
+ ```
562
+
563
+ ### Result:
564
+
565
+ * Only `https://dashboard.myapp.com` is allowed
566
+ * Cookies / sessions allowed
567
+ * Any other origin receives:
568
+
569
+ ```json
570
+ {
571
+ "success": false,
572
+ "error": "Origin Forbidden",
573
+ "message": "Origin \"https://evil.com\" is not allowed"
574
+ }
575
+ ```
576
+
577
+ Status: **403 Forbidden**
578
+
579
+ ---
580
+
581
+ ## ✨ Wildcard Origins on a Route
582
+
583
+ ```js
584
+ app.get("/api/data", {
585
+ cors: {
586
+ origin: "https://*.example.com"
587
+ }
588
+ }, (req, res) => {
589
+ res.ok({ data: 123 });
590
+ });
591
+ ```
592
+
593
+ Matches:
594
+
595
+ * `https://api.example.com`
596
+ * `https://dashboard.example.com`
597
+
598
+ ---
599
+
600
+ ## ⚙️ Full Route-Level CORS Example
601
+
602
+ ```js
603
+ app.get("/user/profile", {
604
+ cors: {
605
+ origin: [
606
+ "https://app.example.com",
607
+ "https://*.trusted.dev"
608
+ ],
609
+ methods: ["GET", "PATCH"],
610
+ headers: ["Content-Type", "Authorization"],
611
+ exposedHeaders: ["X-User-Id"],
612
+ credentials: true,
613
+ maxAge: 600,
614
+ strictOrigin: true,
615
+ allowPrivateNetwork: true,
616
+ debug: true
617
+ }
618
+ }, (req, res) => {
619
+ res.ok({ profile: "OK" });
620
+ });
621
+ ```
622
+
623
+ ---
624
+
625
+ ## 📌 How Route-Level CORS Works Internally
626
+
627
+ When a request hits a route:
628
+
629
+ 1. If global CORS is enabled → apply it.
630
+ 2. If the route defines `cors` → merge with global config.
631
+ 3. Route CORS **overrides** global CORS.
632
+ 4. If request is `OPTIONS` (preflight) → return automatic CORS response.
633
+ 5. Otherwise → run route handler.
634
+
635
+ This ensures predictable behavior.
636
+
637
+ ---
638
+
639
+ ## 📘 Route-Level CORS Options
640
+
641
+ | Option | Type | Description | |
642
+ | --------------------- | ---------- | ---------------------------------------- | ---------------------------------------- |
643
+ | `origin` | `string | string[]` | Allowed origins (`*`, domain, wildcard). |
644
+ | `methods` | `string[]` | Allowed HTTP methods. | |
645
+ | `headers` | `string[]` | Allowed request headers. | |
646
+ | `exposedHeaders` | `string[]` | Response headers exposed to the browser. | |
647
+ | `credentials` | `boolean` | Allow cookies/sessions. | |
648
+ | `maxAge` | `number` | Preflight cache lifetime (seconds). | |
649
+ | `strictOrigin` | `boolean` | Reject non-matching origins with 403. | |
650
+ | `allowPrivateNetwork` | `boolean` | Enables Chrome Private Network Access. | |
651
+ | `debug` | `boolean` | Logs detailed CORS decisions. | |
652
+
653
+ ---
654
+
655
+ ## 🧩 Interaction with Global CORS
656
+
657
+ | Feature | Global | Route |
658
+ | ------------------ | ------ | ------------- |
659
+ | Origin | ✔ | ✔ (overrides) |
660
+ | Wildcards | ✔ | ✔ |
661
+ | Credentials | ✔ | ✔ |
662
+ | Strict Origin | ✔ | ✔ |
663
+ | Private Network | ✔ | ✔ |
664
+ | Debug Logging | ✔ | ✔ |
665
+ | Preflight Handling | ✔ | ✔ |
666
+ | Overrides Global | ❌ | ✔ |
667
+
668
+
669
+
670
+
671
+ # **Enable CORS Debug Logging**
672
+
673
+ ```js
674
+ app.cors({
675
+ debug: true
676
+ });
677
+ ```
678
+
679
+ Console output example:
680
+
681
+ ```
682
+ [CORS DEBUG]
683
+ Request: GET /users
684
+ Origin: https://app.example.com
685
+ Applied CORS Policy:
686
+ - Access-Control-Allow-Origin: https://app.example.com
687
+ - Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
688
+ - Access-Control-Allow-Headers: Content-Type, Authorization
689
+ - Max-Age: 86400
690
+ Simple request handled normally
691
+ ```
692
+
693
+ ---
694
+
695
+ # **Preflight Handling (OPTIONS)**
696
+
697
+ Lieko-Express automatically handles it:
698
+
699
+ ### Request:
700
+
701
+ ```
702
+ OPTIONS /login
703
+ Access-Control-Request-Method: POST
704
+ Access-Control-Request-Headers: Content-Type
705
+ ```
706
+
707
+ ### Lieko Response:
708
+
709
+ ```
710
+ 204 No Content
711
+ Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
712
+ Access-Control-Allow-Headers: Content-Type, Authorization
713
+ Access-Control-Max-Age: 86400
714
+ ```
715
+
716
+ ---
717
+
718
+ # **Credentials Support**
719
+
720
+ ```js
721
+ app.cors({
722
+ credentials: true,
723
+ origin: "https://myapp.com"
724
+ });
725
+ ```
726
+
727
+ Response contains:
728
+
729
+ ```
730
+ Access-Control-Allow-Credentials: true
731
+ ```
732
+
733
+ ---
734
+
735
+
736
+ # **Disable CORS**
737
+
738
+ Just don’t call `app.cors()`.
739
+ CORS stays fully disabled.
740
+
741
+ ---
742
+
743
+ # **Default CORS Configuration**
744
+
745
+ ```js
746
+ this.corsOptions = {
747
+ enabled: false,
748
+ origin: "*",
749
+ strictOrigin: false,
750
+ allowPrivateNetwork: false,
751
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
752
+ headers: ["Content-Type", "Authorization"],
753
+ credentials: false,
754
+ maxAge: 86400,
755
+ exposedHeaders: [],
756
+ debug: false
757
+ };
758
+ ```
385
759
 
386
760
  # 🔍 Request Object
387
761
 
@@ -514,15 +888,83 @@ res.paginated(items, page, limit, total);
514
888
  * Error code → HTTP mapping
515
889
  * String errors also supported (`res.error("Invalid user")`)
516
890
 
517
- # 📦 Body Parsing
891
+ ---
892
+ # Body Parsing (JSON, URL-encoded, Multipart) — 100% Native, Zero Dependencies
893
+
894
+ Lieko Express ships with a **fast, secure, and fully native body parser** — no external packages, not even `body-parser`.
895
+
896
+ As soon as you create your app, body parsing is **automatically enabled**:
897
+
898
+ ```js
899
+ const app = Lieko(); // Ready to handle JSON, form-data, files, etc.
900
+ ```
901
+
902
+ ### Default Limits
903
+
904
+ | Content-Type | Default Limit | Bytes |
905
+ |---------------------------------------|---------------|-------------------|
906
+ | `application/json` | **1mb** | ~1,048,576 bytes |
907
+ | `application/x-www-form-urlencoded` | **1mb** | ~1,048,576 bytes |
908
+ | `multipart/form-data` (file uploads) | **10mb** | ~10,485,760 bytes |
909
+
910
+ That’s already **10× more generous** than Express’s default 100kb!
911
+
912
+ ### Change Limits — Three Super-Simple Ways
913
+
914
+ #### 1. Per content-type (most common)
915
+
916
+ ```js
917
+ app.json({ limit: '100mb' }); // JSON payloads only
918
+ app.urlencoded({ limit: '50mb' }); // Classic HTML forms
919
+ ```
920
+
921
+ #### 2. Global limit (JSON + urlencoded at once)
922
+
923
+ ```js
924
+ app.bodyParser({ limit: '50mb' });
925
+ // Applies 50mb to both JSON and urlencoded bodies
926
+ ```
927
+
928
+ #### 3. File upload limit (multipart/form-data)
929
+
930
+ ```js
931
+ app.multipart({ limit: '500mb' }); // For very large file uploads
932
+ ```
933
+
934
+ You can freely combine them:
935
+
936
+ ```js
937
+ const app = Lieko();
938
+
939
+ app.json({ limit: '10mb' }); // Small, fast JSON APIs
940
+ app.urlencoded({ limit: '5mb' });
941
+ app.multipart({ limit: '1gb' }); // Allow huge file uploads
942
+ ```
943
+
944
+ ### Real-world Example
945
+
946
+ ```js
947
+ const Lieko = require('lieko-express');
948
+ const app = Lieko();
949
+
950
+ // Accept reasonably large JSON payloads
951
+ app.json({ limit: '25mb' });
952
+
953
+ // Accept classic forms
954
+ app.urlencoded({ limit: '10mb' });
518
955
 
519
- Lieko supports:
956
+ // Allow big file uploads (photos, videos, etc.)
957
+ app.multipart({ limit: '500mb' });
520
958
 
521
- ### JSON
959
+ app.post('/upload', (req, res) => {
960
+ console.log('JSON body size :', Buffer.byteLength(JSON.stringify(req.body)), 'bytes');
961
+ console.log('Uploaded files :', Object.keys(req.files || {}));
522
962
 
523
- ### URL-encoded
963
+ res.ok({ received: true, files: req.files });
964
+ });
524
965
 
525
- ### ✔ Multipart form-data (files)
966
+ app.listen(3000);
967
+ ```
526
968
 
527
969
  Uploads end up in:
528
970
 
@@ -546,6 +988,12 @@ Query & body fields are **auto converted**:
546
988
  "null" → null
547
989
  ```
548
990
 
991
+
992
+ **No `app.use(express.json())`, no `app.use(express.urlencoded())`, no extra dependencies** — everything works out of the box, blazing fast, and fully configurable in one line.
993
+
994
+ That’s the Lieko philosophy: **less boilerplate, more power**.
995
+
996
+
549
997
  ### Response Methods
550
998
 
551
999
  ```javascript
@@ -1577,6 +2025,7 @@ DEBUG REQUEST
1577
2025
  → Params: {"id":"42"}
1578
2026
  → Query: {"page":2,"active":true}
1579
2027
  → Body: {}
2028
+ → Body Size: 0 bytes
1580
2029
  → Files:
1581
2030
  ---------------------------------------------
1582
2031
  ```
@@ -1649,9 +2098,9 @@ You can configure:
1649
2098
  ```js
1650
2099
  app.set('trust proxy', value);
1651
2100
  app.set('debug', boolean);
1652
- app.set('x-powered-by', false);
1653
- app.set('strictTrailingSlash', false);
1654
- app.set('allowTrailingSlash', true);
2101
+ app.set('x-powered-by', boolean);
2102
+ app.set('strictTrailingSlash', boolean);
2103
+ app.set('allowTrailingSlash', boolean);
1655
2104
  ```
1656
2105
 
1657
2106
  # 🌐 Trust Proxy & IP Parsing
@@ -1761,15 +2210,6 @@ function myPlugin(app) {
1761
2210
 
1762
2211
  myPlugin(app);
1763
2212
  ```
1764
-
1765
- ## 🚀 Deploying Lieko Express
1766
-
1767
- ### Node.js (PM2)
1768
-
1769
- ```bash
1770
- pm2 start app.js
1771
- ```
1772
-
1773
2213
  ## 📦 API Reference
1774
2214
 
1775
2215
  ### `Lieko()`
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;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "lieko-express",
3
+ "version": "0.0.4",
3
4
  "repository": {
4
5
  "type": "git",
5
6
  "url": "https://github.com/eiwSrvt/lieko-express"
6
7
  },
7
8
  "homepage": "https://github.com/eiwSrvt/lieko-express",
8
- "version": "0.0.3",
9
9
  "description": "Lieko-express — A Modern, Minimal, REST API Framework for Node.js",
10
10
  "main": "lieko-express.js",
11
11
  "scripts": {