lieko-express 0.0.20

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.
@@ -0,0 +1,1926 @@
1
+ const { createServer } = require('http');
2
+ const net = require("net");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const { getMimeType } = require('./helpers/mimes');
7
+
8
+ const {
9
+ Schema,
10
+ ValidationError,
11
+ validators,
12
+ validate,
13
+ validatePartial
14
+ } = require('./lib/schema');
15
+
16
+ process.env.UV_THREADPOOL_SIZE = require('os').availableParallelism();
17
+
18
+ class LiekoExpress {
19
+ constructor() {
20
+ this.groupStack = [];
21
+ this.routes = [];
22
+ this.middlewares = [];
23
+ this.errorHandlers = [];
24
+ this.notFoundHandler = null;
25
+ this.server = null;
26
+
27
+ this.settings = {
28
+ debug: false,
29
+ 'x-powered-by': 'lieko-express',
30
+ 'trust proxy': false,
31
+ strictTrailingSlash: true,
32
+ allowTrailingSlash: true,
33
+ views: path.join(process.cwd(), "views"),
34
+ "view engine": "html"
35
+ };
36
+
37
+ this.engines = {};
38
+ this.engines['.html'] = this._defaultHtmlEngine.bind(this);
39
+
40
+ this.bodyParserOptions = {
41
+ json: {
42
+ limit: '10mb',
43
+ strict: true
44
+ },
45
+ urlencoded: {
46
+ limit: '10mb',
47
+ extended: true
48
+ },
49
+ multipart: {
50
+ limit: '10mb'
51
+ }
52
+ };
53
+
54
+ this.corsOptions = {
55
+ enabled: false,
56
+ origin: "*",
57
+ strictOrigin: false,
58
+ allowPrivateNetwork: false,
59
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
60
+ headers: ["Content-Type", "Authorization"],
61
+ credentials: false,
62
+ maxAge: 86400,
63
+ exposedHeaders: [],
64
+ debug: false
65
+ };
66
+
67
+ this.excludedPatterns = [
68
+ /^\/\.well-known\/.*/i // Chrome DevTools, Apple, etc.
69
+ ]
70
+ }
71
+
72
+ excludeUrl(patterns) {
73
+ if (!Array.isArray(patterns)) patterns = [patterns];
74
+ this.excludedPatterns = this.excludedPatterns || [];
75
+
76
+ patterns.forEach(pattern => {
77
+ if (pattern instanceof RegExp) {
78
+ this.excludedPatterns.push(pattern);
79
+ return;
80
+ }
81
+
82
+ let regexStr = pattern
83
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
84
+ .replace(/\\\*/g, '.*');
85
+
86
+ regexStr = '^' + regexStr + '$';
87
+ this.excludedPatterns.push(new RegExp(regexStr, 'i'));
88
+ });
89
+
90
+ return this;
91
+ }
92
+
93
+ _isExcluded(url) {
94
+ if (!this.excludedPatterns?.length) return false;
95
+ return this.excludedPatterns.some(re => re.test(url));
96
+ }
97
+
98
+ cors(options = {}) {
99
+ if (options === false) {
100
+ this._corsMiddleware = null;
101
+ return this;
102
+ }
103
+
104
+ const middleware = require('./lib/cors')(options);
105
+ this._corsMiddleware = middleware;
106
+
107
+ const stack = new Error().stack;
108
+ if (!stack.includes('at LiekoExpress.use')) {
109
+ this.use(middleware);
110
+ }
111
+
112
+ return middleware;
113
+ }
114
+
115
+ debug(value = true) {
116
+ if (typeof value === 'string') {
117
+ value = value.toLowerCase() === 'true';
118
+ }
119
+ this.set('debug', value);
120
+ return this;
121
+ }
122
+
123
+ set(name, value) {
124
+ this.settings[name] = value;
125
+ return this;
126
+ }
127
+
128
+ engine(ext, renderFunction) {
129
+ if (!ext.startsWith(".")) ext = "." + ext;
130
+ this.engines[ext] = renderFunction;
131
+ return this;
132
+ }
133
+
134
+ enable(name) {
135
+ this.settings[name] = true;
136
+ return this;
137
+ }
138
+
139
+ disable(name) {
140
+ this.settings[name] = false;
141
+ return this;
142
+ }
143
+
144
+ enabled(name) {
145
+ return !!this.settings[name];
146
+ }
147
+
148
+ disabled(name) {
149
+ return !this.settings[name];
150
+ }
151
+
152
+ bodyParser(options = {}) {
153
+ if (options.limit) {
154
+ this.bodyParserOptions.json.limit = options.limit;
155
+ this.bodyParserOptions.urlencoded.limit = options.limit;
156
+ this.bodyParserOptions.multipart.limit = options.limit;
157
+ }
158
+ if (options.extended !== undefined) {
159
+ this.bodyParserOptions.urlencoded.extended = options.extended;
160
+ }
161
+ if (options.strict !== undefined) {
162
+ this.bodyParserOptions.json.strict = options.strict;
163
+ }
164
+ return this;
165
+ }
166
+
167
+ json(options = {}) {
168
+ if (options.limit) {
169
+ this.bodyParserOptions.json.limit = options.limit;
170
+ }
171
+ if (options.strict !== undefined) {
172
+ this.bodyParserOptions.json.strict = options.strict;
173
+ }
174
+ return this;
175
+ }
176
+
177
+ urlencoded(options = {}) {
178
+ if (options.limit) {
179
+ this.bodyParserOptions.urlencoded.limit = options.limit;
180
+ }
181
+ if (options.extended !== undefined) {
182
+ this.bodyParserOptions.urlencoded.extended = options.extended;
183
+ }
184
+ return this;
185
+ }
186
+
187
+ multipart(options = {}) {
188
+ if (options.limit) {
189
+ this.bodyParserOptions.multipart.limit = options.limit;
190
+ }
191
+ return this;
192
+ }
193
+
194
+ _parseLimit(limit) {
195
+ if (typeof limit === 'number') return limit;
196
+
197
+ const match = limit.match(/^(\d+(?:\.\d+)?)(kb|mb|gb)?$/i);
198
+ if (!match) return 1048576;
199
+
200
+ const value = parseFloat(match[1]);
201
+ const unit = (match[2] || 'b').toLowerCase();
202
+
203
+ const multipliers = {
204
+ b: 1,
205
+ kb: 1024,
206
+ mb: 1024 * 1024,
207
+ gb: 1024 * 1024 * 1024
208
+ };
209
+
210
+ return value * multipliers[unit];
211
+ }
212
+
213
+ async _parseBody(req, routeOptions = null) {
214
+ return new Promise((resolve, reject) => {
215
+
216
+ if (['GET', 'DELETE', 'HEAD'].includes(req.method)) {
217
+ req.body = {};
218
+ req.files = {};
219
+ req._bodySize = 0;
220
+ return resolve();
221
+ }
222
+
223
+ const contentType = (req.headers['content-type'] || '').toLowerCase();
224
+ const options = routeOptions || this.bodyParserOptions;
225
+
226
+ req.body = {};
227
+ req.files = {};
228
+
229
+ let raw = Buffer.alloc(0);
230
+ let size = 0;
231
+ let limitExceeded = false;
232
+ let errorSent = false;
233
+
234
+ const detectLimit = () => {
235
+ if (contentType.includes('application/json')) {
236
+ return this._parseLimit(options.json.limit);
237
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
238
+ return this._parseLimit(options.urlencoded.limit);
239
+ } else if (contentType.includes('multipart/form-data')) {
240
+ return this._parseLimit(options.multipart.limit);
241
+ } else {
242
+ return this._parseLimit('1mb');
243
+ }
244
+ };
245
+
246
+ const limit = detectLimit();
247
+ const limitLabel =
248
+ contentType.includes('application/json') ? options.json.limit :
249
+ contentType.includes('application/x-www-form-urlencoded') ? options.urlencoded.limit :
250
+ contentType.includes('multipart/form-data') ? options.multipart.limit :
251
+ '1mb';
252
+
253
+ req.on('data', chunk => {
254
+ if (limitExceeded || errorSent) return;
255
+
256
+ size += chunk.length;
257
+
258
+ if (size > limit) {
259
+ limitExceeded = true;
260
+ errorSent = true;
261
+
262
+ req.removeAllListeners('data');
263
+ req.removeAllListeners('end');
264
+ req.removeAllListeners('error');
265
+
266
+ req.on('data', () => { });
267
+ req.on('end', () => { });
268
+
269
+ const error = new Error(`Request body too large. Limit: ${limitLabel}`);
270
+ error.status = 413;
271
+ error.code = 'PAYLOAD_TOO_LARGE';
272
+ return reject(error);
273
+ }
274
+
275
+ raw = Buffer.concat([raw, chunk]);
276
+ });
277
+
278
+ req.on('end', () => {
279
+ if (limitExceeded) return;
280
+
281
+ req._bodySize = size;
282
+
283
+ try {
284
+
285
+ if (contentType.includes('application/json')) {
286
+ const text = raw.toString();
287
+ try {
288
+ req.body = JSON.parse(text);
289
+
290
+ if (options.json.strict && text.trim() && !['[', '{'].includes(text.trim()[0])) {
291
+ return reject(new Error('Strict mode: body must be an object or array'));
292
+ }
293
+ } catch (err) {
294
+ req.body = {};
295
+ }
296
+ }
297
+
298
+ else if (contentType.includes('application/x-www-form-urlencoded')) {
299
+ const text = raw.toString();
300
+ const params = new URLSearchParams(text);
301
+ req.body = {};
302
+
303
+ if (options.urlencoded.extended) {
304
+ for (const [key, value] of params) {
305
+ if (key.includes('[')) {
306
+ const match = key.match(/^([^\[]+)\[([^\]]*)\]$/);
307
+ if (match) {
308
+ const [, objKey, subKey] = match;
309
+ if (!req.body[objKey]) req.body[objKey] = {};
310
+ if (subKey) req.body[objKey][subKey] = value;
311
+ else {
312
+ if (!Array.isArray(req.body[objKey])) req.body[objKey] = [];
313
+ req.body[objKey].push(value);
314
+ }
315
+ continue;
316
+ }
317
+ }
318
+ req.body[key] = value;
319
+ }
320
+ } else {
321
+ req.body = Object.fromEntries(params);
322
+ }
323
+ }
324
+
325
+ else if (contentType.includes('multipart/form-data')) {
326
+ const boundaryMatch = contentType.match(/boundary=([^;]+)/);
327
+ if (!boundaryMatch) return reject(new Error('Missing multipart boundary'));
328
+
329
+ const boundary = '--' + boundaryMatch[1];
330
+
331
+ const text = raw.toString('binary');
332
+ const parts = text.split(boundary).filter(p => p && !p.includes('--'));
333
+
334
+ for (let part of parts) {
335
+ const headerEnd = part.indexOf('\r\n\r\n');
336
+ if (headerEnd === -1) continue;
337
+
338
+ const headers = part.slice(0, headerEnd);
339
+ const body = part.slice(headerEnd + 4).replace(/\r\n$/, '');
340
+
341
+ const nameMatch = headers.match(/name="([^"]+)"/);
342
+ const filenameMatch = headers.match(/filename="([^"]*)"/);
343
+ const contentTypeMatch = headers.match(/Content-Type:\s*([^\r\n]+)/i);
344
+
345
+ const field = nameMatch?.[1];
346
+ if (!field) continue;
347
+
348
+ if (filenameMatch?.[1]) {
349
+ const bin = Buffer.from(body, 'binary');
350
+
351
+ req.files[field] = {
352
+ filename: filenameMatch[1],
353
+ data: bin,
354
+ size: bin.length,
355
+ contentType: contentTypeMatch ? contentTypeMatch[1] : 'application/octet-stream'
356
+ };
357
+ } else {
358
+ req.body[field] = body;
359
+ }
360
+ }
361
+ }
362
+
363
+ else {
364
+ const text = raw.toString();
365
+ req.body = text ? { text } : {};
366
+ }
367
+
368
+ for (const key in req.body) {
369
+ const value = req.body[key];
370
+
371
+ if (typeof value === 'string' && value.trim() !== '' && !isNaN(value)) {
372
+ req.body[key] = parseFloat(value);
373
+ } else if (value === 'true') {
374
+ req.body[key] = true;
375
+ } else if (value === 'false') {
376
+ req.body[key] = false;
377
+ }
378
+ }
379
+
380
+ resolve();
381
+
382
+ } catch (error) {
383
+ reject(error);
384
+ }
385
+ });
386
+
387
+ req.on('error', reject);
388
+ });
389
+ }
390
+
391
+ get(...args) {
392
+ if (args.length === 1 && typeof args[0] === 'string' && !args[0].startsWith('/')) {
393
+ return this.settings[args[0]];
394
+ } else {
395
+ this._addRoute('GET', ...args);
396
+ return this;
397
+ }
398
+ }
399
+
400
+ post(path, ...handlers) {
401
+ this._addRoute('POST', path, ...handlers);
402
+ return this;
403
+ }
404
+
405
+ put(path, ...handlers) {
406
+ this._addRoute('PUT', path, ...handlers);
407
+ return this;
408
+ }
409
+
410
+ delete(path, ...handlers) {
411
+ this._addRoute('DELETE', path, ...handlers);
412
+ return this;
413
+ }
414
+
415
+ patch(path, ...handlers) {
416
+ this._addRoute('PATCH', path, ...handlers);
417
+ return this;
418
+ }
419
+
420
+ all(path, ...handlers) {
421
+ this._addRoute('ALL', path, ...handlers);
422
+ return this;
423
+ }
424
+
425
+ group(basePath, ...args) {
426
+ const parent = this;
427
+
428
+ const callback = args.pop();
429
+ if (typeof callback !== "function") {
430
+ throw new Error("group() requires a callback as last argument");
431
+ }
432
+
433
+ const middlewares = args.filter(fn => typeof fn === "function");
434
+
435
+ const normalize = (p) => p.replace(/\/+$/, '');
436
+ const fullBase = normalize(basePath);
437
+
438
+ const subApp = {
439
+ _call(method, path, handlers) {
440
+ const finalPath = normalize(fullBase + path);
441
+ parent[method](finalPath, ...middlewares, ...handlers);
442
+ return subApp;
443
+ },
444
+ get(path, ...handlers) { return this._call('get', path, handlers); },
445
+ post(path, ...handlers) { return this._call('post', path, handlers); },
446
+ put(path, ...handlers) { return this._call('put', path, handlers); },
447
+ patch(path, ...handlers) { return this._call('patch', path, handlers); },
448
+ delete(path, ...handlers) { return this._call('delete', path, handlers); },
449
+ all(path, ...handlers) { return this._call('all', path, handlers); },
450
+
451
+ use(pathOrMw, ...rest) {
452
+ if (typeof pathOrMw === 'object' && pathOrMw instanceof LiekoExpress) {
453
+ const finalPath = fullBase === '/' ? '/' : fullBase;
454
+ parent.use(finalPath, ...middlewares, pathOrMw);
455
+ return subApp;
456
+ }
457
+
458
+ if (typeof pathOrMw === "function") {
459
+ parent.use(fullBase, ...middlewares, pathOrMw);
460
+ return subApp;
461
+ }
462
+
463
+ if (typeof pathOrMw === "string") {
464
+ const finalPath = normalize(fullBase + pathOrMw);
465
+ parent.use(finalPath, ...middlewares, ...rest);
466
+ return subApp;
467
+ }
468
+
469
+ throw new Error("Invalid group.use() arguments");
470
+ },
471
+
472
+ group(subPath, ...subArgs) {
473
+ const subCb = subArgs.pop();
474
+ const subMw = subArgs.filter(fn => typeof fn === "function");
475
+
476
+ const finalPath = normalize(fullBase + subPath);
477
+ parent.group(finalPath, ...middlewares, ...subMw, subCb);
478
+ return subApp;
479
+ }
480
+ };
481
+
482
+ this.groupStack.push({ basePath: fullBase, middlewares });
483
+ callback(subApp);
484
+ this.groupStack.pop();
485
+
486
+ return this;
487
+ }
488
+
489
+ _checkMiddleware(handler) {
490
+ const isAsync = handler instanceof (async () => { }).constructor;
491
+
492
+ if (isAsync) return;
493
+
494
+ if (handler.length < 3) {
495
+ const funcString = handler.toString();
496
+ const stack = new Error().stack;
497
+ let userFileInfo = 'unknown location';
498
+ let userLine = '';
499
+
500
+ if (stack) {
501
+ const lines = stack.split('\n');
502
+
503
+ for (let i = 0; i < lines.length; i++) {
504
+ const line = lines[i].trim();
505
+
506
+ if (line.includes('lieko-express.js') ||
507
+ line.includes('_checkMiddleware') ||
508
+ line.includes('at LiekoExpress.') ||
509
+ line.includes('at Object.<anonymous>') ||
510
+ line.includes('at Module._compile')) {
511
+ continue;
512
+ }
513
+
514
+ const fileMatch = line.match(/\(?(.+?):(\d+):(\d+)\)?$/);
515
+ if (fileMatch) {
516
+ const filePath = fileMatch[1];
517
+ const lineNumber = fileMatch[2];
518
+
519
+ const shortPath = filePath.replace(process.cwd(), '.');
520
+ userFileInfo = `${shortPath}:${lineNumber}`;
521
+ userLine = line;
522
+ break;
523
+ }
524
+ }
525
+ }
526
+
527
+ const firstLine = funcString.split('\n')[0];
528
+ const secondLine = funcString.split('\n')[1] || '';
529
+ const thirdLine = funcString.split('\n')[2] || '';
530
+
531
+ const yellow = '\x1b[33m';
532
+ const red = '\x1b[31m';
533
+ const cyan = '\x1b[36m';
534
+ const reset = '\x1b[0m';
535
+ const bold = '\x1b[1m';
536
+
537
+ console.warn(`
538
+ ${yellow}${bold}⚠️ WARNING: Middleware missing 'next' parameter${reset}
539
+ ${yellow}This middleware may block the request pipeline.${reset}
540
+
541
+ ${cyan}📍 Defined at:${reset} ${userFileInfo}
542
+ ${userLine ? `${cyan} Stack trace:${reset} ${userLine}` : ''}
543
+
544
+ ${cyan}🔧 Middleware definition:${reset}
545
+ ${yellow}${firstLine.substring(0, 100)}${firstLine.length > 100 ? '...' : ''}${reset}
546
+ ${secondLine ? `${yellow} ${secondLine.substring(0, 100)}${secondLine.length > 100 ? '...' : ''}${reset}` : ''}
547
+ ${thirdLine ? `${yellow} ${thirdLine.substring(0, 100)}${thirdLine.length > 100 ? '...' : ''}${reset}` : ''}
548
+
549
+ ${red}${bold}FIX:${reset} Add 'next' as third parameter and call it:
550
+ ${cyan} (req, res, next) => {
551
+ // your code here
552
+ next(); // ← Don't forget to call next()
553
+ }${reset}
554
+ `);
555
+ }
556
+ }
557
+
558
+ notFound(handler) {
559
+ this.notFoundHandler = handler;
560
+ return this;
561
+ }
562
+
563
+ errorHandler(handler) {
564
+ if (handler.length !== 4) {
565
+ throw new Error('errorHandler() requires (err, req, res, next)');
566
+ }
567
+ this.errorHandlers.push(handler);
568
+ return this;
569
+ }
570
+
571
+ use(...args) {
572
+ // auto-mount router on "/"
573
+ if (args.length === 1 && args[0] instanceof LiekoExpress) {
574
+ this._mountRouter('/', args[0]);
575
+ return this;
576
+ }
577
+
578
+ // app.use(middleware)
579
+ if (args.length === 1 && typeof args[0] === 'function') {
580
+ this._checkMiddleware(args[0]);
581
+ this.middlewares.push({ path: null, handler: args[0] });
582
+ return this;
583
+ }
584
+
585
+ // app.use(path, middleware)
586
+ if (args.length === 2 && typeof args[0] === 'string' && typeof args[1] === 'function') {
587
+ this._checkMiddleware(args[1]);
588
+ this.middlewares.push({ path: args[0], handler: args[1] });
589
+ return this;
590
+ }
591
+
592
+ // app.use(path, router)
593
+ if (args.length === 2 && typeof args[0] === 'string' && args[1] instanceof LiekoExpress) {
594
+ this._mountRouter(args[0], args[1]);
595
+ return this;
596
+ }
597
+
598
+ // app.use(path, middleware, router)
599
+ if (args.length === 3 && typeof args[0] === 'string' && typeof args[1] === 'function' && args[2] instanceof LiekoExpress) {
600
+ const [path, middleware, router] = args;
601
+ this._checkMiddleware(middleware);
602
+ this.middlewares.push({ path, handler: middleware });
603
+ this._mountRouter(path, router);
604
+ return this;
605
+ }
606
+
607
+ // app.use(path, ...middlewares, router)
608
+ if (args.length >= 3 && typeof args[0] === 'string') {
609
+ const path = args[0];
610
+ const lastArg = args[args.length - 1];
611
+
612
+ if (lastArg instanceof LiekoExpress) {
613
+ const middlewares = args.slice(1, -1);
614
+ middlewares.forEach(mw => {
615
+ if (typeof mw === 'function') {
616
+ this._checkMiddleware(mw);
617
+ this.middlewares.push({ path, handler: mw });
618
+ }
619
+ });
620
+ this._mountRouter(path, lastArg);
621
+ return this;
622
+ }
623
+
624
+ const middlewares = args.slice(1);
625
+ const allFunctions = middlewares.every(mw => typeof mw === 'function');
626
+ if (allFunctions) {
627
+ middlewares.forEach(mw => {
628
+ this._checkMiddleware(mw);
629
+ this.middlewares.push({ path, handler: mw });
630
+ });
631
+ return this;
632
+ }
633
+ }
634
+
635
+ throw new Error('Invalid use() arguments');
636
+ }
637
+
638
+ static(root, options = {}) {
639
+ return require('./lib/static')(root, options);
640
+ }
641
+
642
+ _mountRouter(basePath, router) {
643
+ basePath = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
644
+ router.groupStack = [...this.groupStack];
645
+
646
+ router.routes.forEach(route => {
647
+ const fullPath = route.path === '' ? basePath : basePath + route.path;
648
+
649
+ this.routes.push({
650
+ ...route,
651
+ path: fullPath,
652
+ pattern: this._pathToRegex(fullPath),
653
+ groupChain: [
654
+ ...this.groupStack,
655
+ ...(route.groupChain || [])
656
+ ],
657
+ bodyParserOptions: router.bodyParserOptions
658
+ });
659
+ });
660
+
661
+ router.middlewares.forEach(mw => {
662
+ this.middlewares.push({
663
+ path: basePath === '' ? mw.path : (mw.path ? basePath + mw.path : basePath),
664
+ handler: mw.handler
665
+ });
666
+ });
667
+ }
668
+
669
+ _addRoute(method, path, ...handlers) {
670
+ if (handlers.length === 0) {
671
+ throw new Error('Route handler is required');
672
+ }
673
+
674
+ const finalHandler = handlers[handlers.length - 1];
675
+ if (!finalHandler) {
676
+ throw new Error(`Route handler is undefined for ${method} ${path}`);
677
+ }
678
+
679
+ const routeMiddlewares = handlers.slice(0, -1);
680
+
681
+ routeMiddlewares.forEach(mw => {
682
+ if (typeof mw === 'function') {
683
+ this._checkMiddleware(mw);
684
+ }
685
+ });
686
+
687
+ const paths = Array.isArray(path) ? path : [path];
688
+
689
+ paths.forEach(original => {
690
+ let p = String(original).trim();
691
+ p = p.replace(/\/+/g, '/');
692
+
693
+ if (p !== '/' && p.endsWith('/')) {
694
+ p = p.slice(0, -1);
695
+ }
696
+
697
+ const exists = this.routes.some(r =>
698
+ r.method === method &&
699
+ r.path === p &&
700
+ r.handler === finalHandler
701
+ );
702
+
703
+ if (exists) return;
704
+
705
+ this.routes.push({
706
+ method,
707
+ path: p,
708
+ originalPath: original,
709
+ handler: finalHandler,
710
+ handlerName: (finalHandler && finalHandler.name) || 'anonymous',
711
+ middlewares: routeMiddlewares,
712
+ pattern: this._pathToRegex(p),
713
+ allowTrailingSlash: this.settings.allowTrailingSlash ?? false,
714
+ groupChain: [...this.groupStack]
715
+ });
716
+ });
717
+ }
718
+
719
+ _pathToRegex(path) {
720
+ let p = String(path).trim();
721
+ p = p.replace(/\/+/g, '/');
722
+
723
+ if (p !== '/' && p.endsWith('/')) {
724
+ p = p.slice(0, -1);
725
+ }
726
+
727
+ let pattern = p
728
+ .replace(/:(\w+)/g, '(?<$1>[^/]+)')
729
+ .replace(/\*/g, '.*');
730
+
731
+ const isStatic = !/[:*]/.test(p) && p !== '/';
732
+
733
+ const allowTrailing = this.settings.allowTrailingSlash !== false;
734
+
735
+ if (isStatic && allowTrailing) {
736
+ pattern += '/?';
737
+ }
738
+
739
+ if (p === '/') {
740
+ return /^\/?$/;
741
+ }
742
+
743
+ return new RegExp(`^${pattern}$`);
744
+ }
745
+
746
+ _findRoute(method, pathname) {
747
+ for (const route of this.routes) {
748
+ if (route.method !== method && route.method !== 'ALL') continue;
749
+
750
+ const match = pathname.match(route.pattern);
751
+ if (match) {
752
+ return { ...route, params: match.groups || {}, matchedPath: pathname };
753
+ }
754
+ }
755
+
756
+ if (pathname.endsWith('/') && pathname.length > 1) {
757
+ const cleanPath = pathname.slice(0, -1);
758
+ for (const route of this.routes) {
759
+ if (route.method !== method && route.method !== 'ALL') continue;
760
+
761
+ if (route.path === cleanPath && route.allowTrailingSlash !== false) {
762
+ const match = cleanPath.match(route.pattern);
763
+ if (match) {
764
+ return {
765
+ ...route,
766
+ params: match.groups || {},
767
+ matchedPath: cleanPath,
768
+ wasTrailingSlash: true
769
+ };
770
+ }
771
+ }
772
+ }
773
+ }
774
+ return null;
775
+ }
776
+
777
+ async _runErrorHandlers(err, req, res) {
778
+ if (this.errorHandlers.length === 0) {
779
+ console.error("\n🔥 INTERNAL ERROR");
780
+ console.error(err.stack || err);
781
+ return res.status(500).json({
782
+ error: {
783
+ message: "Internal Server Error",
784
+ code: 500
785
+ }
786
+ });
787
+ }
788
+
789
+ let index = 0;
790
+
791
+ const runNext = async () => {
792
+ const handler = this.errorHandlers[index++];
793
+ if (!handler) return;
794
+
795
+ return new Promise((resolve, reject) => {
796
+ try {
797
+ handler(err, req, res, (nextErr) => {
798
+ if (nextErr) reject(nextErr);
799
+ else resolve(runNext());
800
+ });
801
+ } catch (e) {
802
+ reject(e);
803
+ }
804
+ });
805
+ };
806
+
807
+ try {
808
+ await runNext();
809
+ } catch (e) {
810
+ console.error("\n🔥 ERROR INSIDE ERROR HANDLER");
811
+ console.error(e.stack || e);
812
+ res.status(500).json({
813
+ error: {
814
+ message: "Internal Server Error",
815
+ code: 500
816
+ }
817
+ });
818
+ }
819
+ }
820
+
821
+ error(res, errorObj) {
822
+ if (typeof errorObj === "string") {
823
+ errorObj = { message: errorObj };
824
+ }
825
+
826
+ if (!errorObj || typeof errorObj !== "object") {
827
+ return res.status(500).json({
828
+ error: {
829
+ message: "Invalid error format passed to res.error()",
830
+ code: 500
831
+ }
832
+ });
833
+ }
834
+
835
+ const HTTP_STATUS = {
836
+ // 4xx – CLIENT ERRORS
837
+ INVALID_REQUEST: 400,
838
+ VALIDATION_FAILED: 400,
839
+ NO_TOKEN_PROVIDED: 401,
840
+ INVALID_TOKEN: 401,
841
+ FORBIDDEN: 403,
842
+ NOT_FOUND: 404,
843
+ METHOD_NOT_ALLOWED: 405,
844
+ CONFLICT: 409,
845
+ RECORD_EXISTS: 409,
846
+ TOO_MANY_REQUESTS: 429,
847
+
848
+ // 5xx – SERVER ERRORS
849
+ SERVER_ERROR: 500,
850
+ SERVICE_UNAVAILABLE: 503
851
+ };
852
+
853
+ const status = errorObj.status || HTTP_STATUS[errorObj.code] || 500;
854
+
855
+ return res.status(status).json({
856
+ error: {
857
+ message: errorObj.message || 'An error occurred',
858
+ code: errorObj.code || 500,
859
+ ...errorObj
860
+ }
861
+ });
862
+ }
863
+
864
+ _parseIp(rawIp) {
865
+ if (!rawIp) return { raw: null, ipv4: null, ipv6: null };
866
+ let ip = rawIp.trim();
867
+
868
+ if (ip === '::1') {
869
+ ip = '127.0.0.1';
870
+ }
871
+
872
+ if (ip.startsWith('::ffff:')) {
873
+ ip = ip.slice(7);
874
+ }
875
+
876
+ const family = net.isIP(ip);
877
+
878
+ if (family === 0) {
879
+ return { raw: rawIp, ipv4: null, ipv6: null };
880
+ }
881
+
882
+ return {
883
+ raw: rawIp,
884
+ ipv4: family === 4 ? ip : null,
885
+ ipv6: family === 6 ? ip : null,
886
+ };
887
+ }
888
+
889
+ _isTrustedProxy(ip) {
890
+ const trust = this.settings['trust proxy'];
891
+
892
+ if (!trust) return false;
893
+
894
+ if (trust === true) return true;
895
+
896
+ if (trust === 'loopback') {
897
+ return ip === '127.0.0.1' || ip === '::1';
898
+ }
899
+
900
+ if (typeof trust === 'string') {
901
+ return ip === trust;
902
+ }
903
+
904
+ if (Array.isArray(trust)) {
905
+ return trust.includes(ip);
906
+ }
907
+
908
+ if (typeof trust === 'function') {
909
+ return trust(ip);
910
+ }
911
+
912
+ return false;
913
+ }
914
+
915
+ async _handleRequest(req, res) {
916
+ if (this._isExcluded(req.url.split('?')[0])) {
917
+ res.statusCode = 404;
918
+ return res.end();
919
+ }
920
+
921
+ this._enhanceRequest(req);
922
+
923
+ const url = req.url;
924
+ const qIndex = url.indexOf('?');
925
+ const pathname = qIndex === -1 ? url : url.substring(0, qIndex);
926
+
927
+ const query = {};
928
+ if (qIndex !== -1) {
929
+ const searchParams = new URLSearchParams(url.substring(qIndex + 1));
930
+ for (const [key, value] of searchParams) query[key] = value;
931
+ }
932
+ req.query = query;
933
+ req.params = {};
934
+
935
+ for (const key in req.query) {
936
+ const v = req.query[key];
937
+ if (v === 'true') req.query[key] = true;
938
+ else if (v === 'false') req.query[key] = false;
939
+ else if (/^\d+$/.test(v)) req.query[key] = parseInt(v);
940
+ else if (/^\d+\.\d+$/.test(v)) req.query[key] = parseFloat(v);
941
+ }
942
+
943
+ req._startTime = process.hrtime.bigint();
944
+ this._enhanceResponse(req, res);
945
+
946
+ req.originalUrl = url;
947
+
948
+ try {
949
+ if (req.method === "OPTIONS" && this.corsOptions.enabled) {
950
+ this._applyCors(req, res, this.corsOptions);
951
+ return;
952
+ }
953
+
954
+ const route = this._findRoute(req.method, pathname);
955
+
956
+ if (route) {
957
+ if (route.cors === false) {
958
+ } else if (route.cors) {
959
+ const finalCors = {
960
+ ...this.corsOptions,
961
+ enabled: true,
962
+ ...route.cors
963
+ };
964
+
965
+ this._applyCors(req, res, finalCors);
966
+ if (req.method === "OPTIONS") return;
967
+ } else if (this.corsOptions.enabled) {
968
+ this._applyCors(req, res, this.corsOptions);
969
+ if (req.method === "OPTIONS") return;
970
+ }
971
+ } else {
972
+ if (this.corsOptions.enabled) {
973
+ this._applyCors(req, res, this.corsOptions);
974
+ if (req.method === "OPTIONS") return;
975
+ }
976
+ }
977
+
978
+ try {
979
+ await this._parseBody(req, route ? route.bodyParserOptions : null);
980
+ } catch (error) {
981
+ if (error.code === 'PAYLOAD_TOO_LARGE') {
982
+ return res.status(413).json({
983
+ error: {
984
+ message: 'Payload Too Large',
985
+ code: 413
986
+ }
987
+ });
988
+ }
989
+ return await this._runErrorHandlers(error, req, res);
990
+ }
991
+
992
+ for (const mw of this.middlewares) {
993
+ if (res.headersSent) return;
994
+
995
+ let shouldExecute = false;
996
+ let pathToStrip = '';
997
+
998
+ if (mw.path === null) {
999
+ shouldExecute = true;
1000
+ } else if (url.startsWith(mw.path)) {
1001
+ shouldExecute = true;
1002
+ pathToStrip = mw.path;
1003
+ }
1004
+
1005
+ if (!shouldExecute) continue;
1006
+
1007
+ await new Promise((resolve, reject) => {
1008
+ const currentUrl = req.url;
1009
+
1010
+ if (pathToStrip) {
1011
+ req.url = url.substring(pathToStrip.length) || '/';
1012
+ }
1013
+
1014
+ const next = async (err) => {
1015
+ req.url = currentUrl;
1016
+
1017
+ if (err) {
1018
+ await this._runErrorHandlers(err, req, res);
1019
+ return resolve();
1020
+ }
1021
+ resolve();
1022
+ };
1023
+
1024
+ const result = mw.handler(req, res, next);
1025
+ if (result && typeof result.then === 'function') {
1026
+ result.then(resolve).catch(reject);
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ if (res.headersSent) return;
1032
+
1033
+ if (!route) {
1034
+ if (this.notFoundHandler) return this.notFoundHandler(req, res);
1035
+ return res.status(404).json({ error: { message: 'Route not found', code: 404 } });
1036
+ }
1037
+
1038
+ req.params = route.params;
1039
+
1040
+ for (const middleware of route.middlewares) {
1041
+ if (res.headersSent) return;
1042
+
1043
+ await new Promise((resolve, reject) => {
1044
+ const next = async (err) => {
1045
+ if (err) {
1046
+ await this._runErrorHandlers(err, req, res);
1047
+ return resolve();
1048
+ }
1049
+ resolve();
1050
+ };
1051
+
1052
+ const result = middleware(req, res, next);
1053
+ if (result && typeof result.then === 'function') {
1054
+ result.then(resolve).catch(reject);
1055
+ }
1056
+ });
1057
+ }
1058
+
1059
+ if (res.headersSent) return;
1060
+
1061
+ await route.handler(req, res);
1062
+
1063
+ } catch (error) {
1064
+ if (!res.headersSent) {
1065
+ await this._runErrorHandlers(error, req, res);
1066
+ } else {
1067
+ console.error("UNCAUGHT ERROR AFTER RESPONSE SENT:", error);
1068
+ }
1069
+ }
1070
+ }
1071
+
1072
+ _enhanceRequest(req) {
1073
+ req.app = this;
1074
+ let remoteIp = req.connection?.remoteAddress ||
1075
+ req.socket?.remoteAddress ||
1076
+ '';
1077
+
1078
+ const forwardedFor = req.headers['x-forwarded-for'];
1079
+ let clientIp = remoteIp;
1080
+ let ipsChain = [remoteIp];
1081
+
1082
+ if (forwardedFor) {
1083
+ const chain = forwardedFor
1084
+ .split(',')
1085
+ .map(s => s.trim())
1086
+ .filter(Boolean);
1087
+
1088
+ if (chain.length > 0 && this._isTrustedProxy(remoteIp)) {
1089
+ clientIp = chain[0];
1090
+ ipsChain = chain;
1091
+ }
1092
+ }
1093
+
1094
+ req.ip = this._parseIp(clientIp);
1095
+ req.ips = ipsChain;
1096
+ req.ip.display = req.ip.ipv4 ?? '127.0.0.1';
1097
+ req.protocol = (req.headers['x-forwarded-proto'] || 'http').split(',')[0].trim();
1098
+ req.secure = req.protocol === 'https';
1099
+
1100
+ const host = req.headers['host'];
1101
+ if (host) {
1102
+ const [hostname] = host.split(':');
1103
+ req.hostname = hostname;
1104
+ req.subdomains = hostname.split('.').slice(0, -2).reverse();
1105
+ } else {
1106
+ req.hostname = '';
1107
+ req.subdomains = [];
1108
+ }
1109
+
1110
+ req.originalUrl = req.url;
1111
+ req.path = req.url.split('?')[0];
1112
+ req.xhr = (req.headers['x-requested-with'] || '').toLowerCase() === 'xmlhttprequest';
1113
+
1114
+ req.get = (name) => {
1115
+ if (typeof name !== 'string') return undefined;
1116
+ const lower = name.toLowerCase();
1117
+ for (const key in req.headers) {
1118
+ if (key.toLowerCase() === lower) return req.headers[key];
1119
+ }
1120
+ return undefined;
1121
+ };
1122
+ req.header = req.get;
1123
+
1124
+ const parseAccept = (header) => {
1125
+ if (!header) return [];
1126
+ return header
1127
+ .split(',')
1128
+ .map(part => {
1129
+ const [type, ...rest] = part.trim().split(';');
1130
+ const q = rest
1131
+ .find(p => p.trim().startsWith('q='))
1132
+ ?.split('=')[1];
1133
+ const quality = q ? parseFloat(q) : 1.0;
1134
+ return { type: type.trim().toLowerCase(), quality };
1135
+ })
1136
+ .filter(item => item.quality > 0)
1137
+ .sort((a, b) => b.quality - a.quality)
1138
+ .map(item => item.type);
1139
+ };
1140
+
1141
+ const accepts = (types) => {
1142
+ if (!Array.isArray(types)) types = [types];
1143
+ const accepted = parseAccept(req.headers['accept']);
1144
+
1145
+ for (const type of types) {
1146
+ const t = type.toLowerCase();
1147
+
1148
+ if (accepted.includes(t)) return type;
1149
+
1150
+ if (accepted.some(a => {
1151
+ if (a === '*/*') return true;
1152
+ if (a.endsWith('/*')) {
1153
+ const prefix = a.slice(0, -1);
1154
+ return t.startsWith(prefix);
1155
+ }
1156
+ return false;
1157
+ })) {
1158
+ return type;
1159
+ }
1160
+ }
1161
+
1162
+ return false;
1163
+ };
1164
+
1165
+ req.accepts = function (types) {
1166
+ return accepts(types);
1167
+ };
1168
+
1169
+ req.acceptsLanguages = function (langs) {
1170
+ if (!Array.isArray(langs)) langs = [langs];
1171
+ const accepted = parseAccept(req.headers['accept-language'] || '');
1172
+ for (const lang of langs) {
1173
+ const l = lang.toLowerCase();
1174
+ if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
1175
+ }
1176
+ return false;
1177
+ };
1178
+
1179
+ req.acceptsEncodings = function (encodings) {
1180
+ if (!Array.isArray(encodings)) encodings = [encodings];
1181
+ const accepted = parseAccept(req.headers['accept-encoding'] || '');
1182
+ for (const enc of encodings) {
1183
+ if (accepted.includes(enc.toLowerCase())) return enc;
1184
+ }
1185
+ return false;
1186
+ };
1187
+
1188
+ req.acceptsCharsets = function (charsets) {
1189
+ if (!Array.isArray(charsets)) charsets = [charsets];
1190
+ const accepted = parseAccept(req.headers['accept-charset'] || '');
1191
+ for (const charset of charsets) {
1192
+ if (accepted.includes(charset.toLowerCase())) return charset;
1193
+ }
1194
+ return false;
1195
+ };
1196
+
1197
+ req.is = function (type) {
1198
+ const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
1199
+ if (!type) return ct;
1200
+ const t = type.toLowerCase();
1201
+ if (t.includes('/')) return ct === t;
1202
+ if (t === 'json') return ct.includes('json');
1203
+ if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
1204
+ if (t === 'multipart') return ct.includes('multipart');
1205
+ return false;
1206
+ };
1207
+
1208
+ req.bearer = req.headers.authorization
1209
+ ?.startsWith('Bearer ')
1210
+ ? req.headers.authorization.slice(7).trim()
1211
+ : null;
1212
+
1213
+ /**
1214
+ * Passport-compatible logout (Passport 0.6+ / 0.7)
1215
+ * This ensures logout(cb) always calls cb(null) and never overwrites Express res
1216
+ */
1217
+ req.logout = function logout(callback) {
1218
+ req.user = null;
1219
+
1220
+ // Remove passport session field if it exists
1221
+ if (req.session && req.session.passport) {
1222
+ delete req.session.passport;
1223
+ }
1224
+
1225
+ // Passport v0.6+ expects async logout
1226
+ if (typeof callback === "function") {
1227
+ return callback(null);
1228
+ }
1229
+
1230
+ return Promise.resolve();
1231
+ };
1232
+ }
1233
+
1234
+ _enhanceResponse(req, res) {
1235
+ res.app = this;
1236
+ res.locals = {};
1237
+ let responseSent = false;
1238
+ let statusCode = 200;
1239
+
1240
+ const getDateHeader = (() => {
1241
+ let cachedDate = '';
1242
+ let lastTimestamp = 0;
1243
+
1244
+ return () => {
1245
+ const now = Date.now();
1246
+ if (now !== lastTimestamp) {
1247
+ lastTimestamp = now;
1248
+ cachedDate = new Date(now).toUTCString();
1249
+ }
1250
+ return cachedDate;
1251
+ };
1252
+ })();
1253
+
1254
+ const buildHeaders = (contentType, length) => {
1255
+ const poweredBy = this.settings['x-powered-by'];
1256
+ const shouldShowPoweredBy = poweredBy !== false;
1257
+
1258
+ return {
1259
+ 'Content-Type': contentType,
1260
+ 'Content-Length': length,
1261
+ 'Date': getDateHeader(),
1262
+ 'Connection': 'keep-alive',
1263
+ 'Cache-Control': 'no-store',
1264
+ ...(shouldShowPoweredBy && {
1265
+ 'X-Powered-By': poweredBy === true || poweredBy === undefined
1266
+ ? 'lieko-express'
1267
+ : poweredBy
1268
+ })
1269
+ };
1270
+ };
1271
+
1272
+ res.status = (code) => {
1273
+ statusCode = code;
1274
+ res.statusCode = code;
1275
+ return res;
1276
+ };
1277
+
1278
+ const originalSetHeader = res.setHeader.bind(res);
1279
+
1280
+ res.setHeader = function (name, value) {
1281
+ originalSetHeader(name, value);
1282
+ return this;
1283
+ };
1284
+
1285
+ res.set = function (name, value) {
1286
+ if (arguments.length === 1 && typeof name === 'object' && name !== null) {
1287
+ Object.entries(name).forEach(([k, v]) => originalSetHeader(k, v));
1288
+ } else {
1289
+ originalSetHeader(name, value);
1290
+ }
1291
+ return this;
1292
+ };
1293
+ res.header = res.setHeader;
1294
+
1295
+ res.removeHeader = function (name) {
1296
+ res.removeHeader(name);
1297
+ return res;
1298
+ };
1299
+
1300
+ res.type = function (mime) {
1301
+ res.setHeader("Content-Type", mime);
1302
+ return res;
1303
+ };
1304
+
1305
+ res.render = async (view, options = {}, callback) => {
1306
+ if (responseSent) return res;
1307
+
1308
+ try {
1309
+ const locals = { ...res.locals, ...options };
1310
+ let viewPath = view;
1311
+ let ext = path.extname(view);
1312
+
1313
+ if (!ext) {
1314
+ ext = this.settings['view engine'];
1315
+ if (!ext) {
1316
+ ext = '.html';
1317
+ viewPath = view + ext;
1318
+ } else {
1319
+ if (!ext.startsWith('.')) ext = '.' + ext;
1320
+ viewPath = view + ext;
1321
+ }
1322
+ }
1323
+
1324
+ const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
1325
+ let fullPath = path.join(viewsDir, viewPath);
1326
+ let fileExists = false;
1327
+ try {
1328
+ await fs.promises.access(fullPath);
1329
+ fileExists = true;
1330
+ } catch (err) {
1331
+ const extensions = ['.html', '.ejs', '.pug', '.hbs'];
1332
+ for (const tryExt of extensions) {
1333
+ if (tryExt === ext) continue;
1334
+ const tryPath = fullPath.replace(new RegExp(ext.replace('.', '\\.') + '$'), tryExt);
1335
+ try {
1336
+ await fs.promises.access(tryPath);
1337
+ fullPath = tryPath;
1338
+ ext = tryExt;
1339
+ fileExists = true;
1340
+ break;
1341
+ } catch (err2) { }
1342
+ }
1343
+ }
1344
+
1345
+ if (!fileExists) {
1346
+ const error = new Error(
1347
+ `View "${view}" not found in views directory "${viewsDir}".\n` +
1348
+ `Tried: ${fullPath}`
1349
+ );
1350
+ error.code = 'ENOENT';
1351
+ if (callback) return callback(error);
1352
+ throw error;
1353
+ }
1354
+
1355
+ const renderEngine = this.engines[ext];
1356
+
1357
+ if (!renderEngine) {
1358
+ throw new Error(
1359
+ `No engine registered for extension "${ext}".\n` +
1360
+ `Use app.engine("${ext}", renderFunction) to register one.`
1361
+ );
1362
+ }
1363
+
1364
+ return new Promise((resolve, reject) => {
1365
+ renderEngine(fullPath, locals, (err, html) => {
1366
+ if (err) {
1367
+ if (callback) {
1368
+ callback(err);
1369
+ resolve();
1370
+ } else {
1371
+ reject(err);
1372
+ }
1373
+ return;
1374
+ }
1375
+
1376
+ if (callback) {
1377
+ callback(null, html);
1378
+ resolve();
1379
+ } else {
1380
+ res.html(html);
1381
+ resolve();
1382
+ }
1383
+ });
1384
+ });
1385
+
1386
+ } catch (error) {
1387
+ if (callback) {
1388
+ callback(error);
1389
+ } else {
1390
+ throw error;
1391
+ }
1392
+ }
1393
+ };
1394
+
1395
+ res.json = (data) => {
1396
+ if (responseSent) return res;
1397
+
1398
+ const json = JSON.stringify(data);
1399
+ const length = Buffer.byteLength(json);
1400
+
1401
+ res.writeHead(statusCode || 200, buildHeaders('application/json; charset=utf-8', length));
1402
+
1403
+ responseSent = true;
1404
+ return res.end(json);
1405
+ };
1406
+
1407
+ res.send = (data) => {
1408
+ if (responseSent) return res;
1409
+
1410
+ let body, contentType;
1411
+
1412
+ if (data === null) {
1413
+ body = 'null';
1414
+ contentType = 'application/json; charset=utf-8';
1415
+ } else if (typeof data === 'object') {
1416
+ body = JSON.stringify(data);
1417
+ contentType = 'application/json; charset=utf-8';
1418
+ } else if (typeof data === 'string') {
1419
+ body = data;
1420
+ contentType = 'text/plain; charset=utf-8';
1421
+ } else {
1422
+ body = String(data);
1423
+ contentType = 'text/plain; charset=utf-8';
1424
+ }
1425
+
1426
+ const length = Buffer.byteLength(body);
1427
+
1428
+ res.writeHead(statusCode || 200, buildHeaders(contentType, length));
1429
+
1430
+ responseSent = true;
1431
+ return res.end(body);
1432
+ };
1433
+
1434
+ res.sendFile = async function (filePath, options = {}, callback) {
1435
+ if (responseSent) {
1436
+ if (callback) callback(new Error('Response already sent'));
1437
+ return res;
1438
+ }
1439
+
1440
+ const opts = {
1441
+ maxAge: 0,
1442
+ lastModified: true,
1443
+ headers: {},
1444
+ dotfiles: 'ignore', // 'allow', 'deny', 'ignore'
1445
+ acceptRanges: true,
1446
+ root: null,
1447
+ ...options
1448
+ };
1449
+
1450
+ let file = filePath;
1451
+ if (opts.root) {
1452
+ file = path.join(opts.root, filePath);
1453
+ } else if (!path.isAbsolute(file)) {
1454
+ file = path.resolve(process.cwd(), file);
1455
+ }
1456
+
1457
+ const base = opts.root || process.cwd();
1458
+ if (!file.startsWith(base + path.sep) && !file.startsWith(base)) {
1459
+ const err = new Error('Forbidden path');
1460
+ err.code = 'FORBIDDEN';
1461
+ return handleError(err, 403, 'Forbidden');
1462
+ }
1463
+
1464
+ const basename = path.basename(file);
1465
+ if (opts.dotfiles === 'ignore' && basename.startsWith('.')) {
1466
+ const err = new Error('File not found');
1467
+ err.code = 'ENOENT';
1468
+ return handleError(err, 404, 'Not Found');
1469
+ }
1470
+ if (opts.dotfiles === 'deny' && basename.startsWith('.')) {
1471
+ const err = new Error('Forbidden');
1472
+ err.code = 'FORBIDDEN';
1473
+ return handleError(err, 403, 'Forbidden');
1474
+ }
1475
+
1476
+ try {
1477
+ const stat = await fs.promises.stat(file);
1478
+ if (!stat.isFile()) {
1479
+ const err = new Error('Not a file');
1480
+ err.code = 'ENOENT';
1481
+ return handleError(err, 404, 'Not Found');
1482
+ }
1483
+
1484
+ const contentType = getMimeType(file);
1485
+ const fileSize = stat.size;
1486
+
1487
+ let start = 0;
1488
+ let end = fileSize - 1;
1489
+ const range = req.headers.range;
1490
+
1491
+ if (range) {
1492
+ const parts = range.replace(/bytes=/, '').split('-');
1493
+ start = parseInt(parts[0], 10) || 0;
1494
+ end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
1495
+
1496
+ if (start >= fileSize || end < start || isNaN(start)) {
1497
+ res.status(416);
1498
+ res.setHeader('Content-Range', `bytes */${fileSize}`);
1499
+ return handleError(new Error('Range Not Satisfiable'), 416, 'Range Not Satisfiable');
1500
+ }
1501
+ end = Math.min(end, fileSize - 1);
1502
+ const chunkSize = end - start + 1;
1503
+
1504
+ res.status(206);
1505
+ res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
1506
+ res.setHeader('Content-Length', chunkSize);
1507
+ } else {
1508
+ res.setHeader('Content-Length', fileSize);
1509
+ }
1510
+
1511
+ res.setHeader('Content-Type', contentType);
1512
+ res.setHeader('Accept-Ranges', 'bytes');
1513
+ if (opts.lastModified) res.setHeader('Last-Modified', stat.mtime.toUTCString());
1514
+ if (opts.maxAge) res.setHeader('Cache-Control', `public, max-age=${opts.maxAge}`);
1515
+ Object.entries(opts.headers).forEach(([k, v]) => res.setHeader(k, v));
1516
+
1517
+
1518
+ const stream = fs.createReadStream(file, { start, end });
1519
+ stream.on('error', err => {
1520
+ if (!responseSent) handleError(err, 500, 'Error reading file');
1521
+ });
1522
+ stream.on('end', () => {
1523
+ responseSent = true;
1524
+ if (callback) callback(null);
1525
+ });
1526
+ stream.pipe(res);
1527
+
1528
+ } catch (err) {
1529
+ handleError(err, err.code === 'ENOENT' ? 404 : 500);
1530
+ }
1531
+
1532
+ function handleError(err, status = 500, defaultMessage = 'Server Error') {
1533
+ responseSent = true;
1534
+ let message = defaultMessage;
1535
+ let details = '';
1536
+
1537
+ if (err.code === 'ENOENT') {
1538
+ status = 404;
1539
+ message = 'File Not Found';
1540
+ details = `The file "${filePath}" does not exist.\nFull path tried: ${file}`;
1541
+ } else if (err.code === 'FORBIDDEN') {
1542
+ status = 403;
1543
+ message = 'Forbidden';
1544
+ details = `Access denied to the file "${filePath}".`;
1545
+ } else if (err.code === 'EACCES') {
1546
+ status = 403;
1547
+ message = 'Permission Denied';
1548
+ details = `No read permissions on "${filePath}".`;
1549
+ }
1550
+
1551
+ if (err.stack) {
1552
+ details += `\n\nError: ${err.message}\nStack:\n${err.stack}`;
1553
+ }
1554
+
1555
+ res.status(status);
1556
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
1557
+ res.end(`${message}\n${details.trim()}`);
1558
+
1559
+ if (callback) callback(err);
1560
+ }
1561
+
1562
+ return res;
1563
+ };
1564
+
1565
+ res.html = function (html, status) {
1566
+ res.statusCode = status !== undefined ? status : (statusCode || 200);
1567
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1568
+ res.end(html);
1569
+ };
1570
+
1571
+ res.ok = (data, message) => {
1572
+ if (!res.statusCode || res.statusCode === 200) {
1573
+ res.status(200);
1574
+ }
1575
+ const payload = { data };
1576
+ if (message !== undefined) payload.message = message;
1577
+ return res.json(payload);
1578
+ };
1579
+ //res.success = res.ok;
1580
+
1581
+ res.created = (data, message = 'Resource created successfully') => {
1582
+ const payload = { success: true, data, message };
1583
+ return res.status(201).json(payload);
1584
+ };
1585
+
1586
+ res.noContent = () => {
1587
+ return res.status(204).end();
1588
+ };
1589
+
1590
+ res.accepted = (data = null, message = 'Request accepted') => {
1591
+ return res.status(202).json({ data, message });
1592
+ };
1593
+
1594
+ res.paginated = (items, total, message = 'Data retrieved successfully') => {
1595
+ const page = Math.max(1, Number(req.query.page) || 1);
1596
+ const limit = Math.min(100, Math.max(1, Number(req.query.limit) || 10));
1597
+ const totalPages = Math.ceil(total / limit);
1598
+
1599
+ return res.status(200).json({
1600
+ data: items,
1601
+ message,
1602
+ pagination: {
1603
+ page,
1604
+ limit,
1605
+ total,
1606
+ totalPages,
1607
+ hasNext: page < totalPages,
1608
+ hasPrev: page > 1
1609
+ }
1610
+ });
1611
+ };
1612
+
1613
+ res.redirect = (url, status = 302) => {
1614
+ responseSent = true;
1615
+ res.writeHead(status, { Location: url });
1616
+ res.end();
1617
+ };
1618
+
1619
+ res.error = (obj) => this.error(res, obj);
1620
+ res.fail = res.error;
1621
+
1622
+ res.badRequest = function (msg = "BAD_REQUEST") {
1623
+ return res.status(400).error(msg);
1624
+ };
1625
+
1626
+ res.unauthorized = function (msg = "UNAUTHORIZED") {
1627
+ return res.status(401).error(msg);
1628
+ };
1629
+
1630
+ res.forbidden = function (msg = "FORBIDDEN") {
1631
+ return res.status(403).error(msg);
1632
+ };
1633
+
1634
+ res.notFound = function (msg = "NOT_FOUND") {
1635
+ return res.status(404).error(msg);
1636
+ };
1637
+
1638
+ res.serverError = function (msg = "SERVER_ERROR") {
1639
+ return res.status(500).error(msg);
1640
+ };
1641
+
1642
+ res.cookie = (name, value, options = {}) => {
1643
+ const opts = {
1644
+ path: '/',
1645
+ httpOnly: true,
1646
+ secure: req.secure || false,
1647
+ sameSite: 'lax',
1648
+ maxAge: null,
1649
+ expires: null,
1650
+ ...options
1651
+ };
1652
+
1653
+ let cookie = `${name}=${encodeURIComponent(value)}`;
1654
+
1655
+ if (opts.maxAge) cookie += `; Max-Age=${Math.floor(opts.maxAge / 1000)}`;
1656
+ if (opts.expires) cookie += `; Expires=${opts.expires.toUTCString()}`;
1657
+ cookie += `; Path=${opts.path}`;
1658
+ if (opts.domain) cookie += `; Domain=${opts.domain}`;
1659
+ if (opts.httpOnly) cookie += '; HttpOnly';
1660
+ if (opts.secure) cookie += '; Secure';
1661
+ if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`;
1662
+
1663
+ res.setHeader('Set-Cookie', cookie);
1664
+ return res;
1665
+ };
1666
+
1667
+ res.clearCookie = (name, options = {}) => {
1668
+ if (responseSent) return res;
1669
+
1670
+ const opts = {
1671
+ path: '/',
1672
+ httpOnly: true,
1673
+ secure: req.secure || false,
1674
+ sameSite: 'lax',
1675
+ ...options,
1676
+ expires: new Date(1),
1677
+ maxAge: 0
1678
+ };
1679
+
1680
+ let cookieString = `${name}=; Path=${opts.path}; Expires=${opts.expires.toUTCString()}; Max-Age=0`;
1681
+
1682
+ if (opts.httpOnly) cookieString += '; HttpOnly';
1683
+ if (opts.secure) cookieString += '; Secure';
1684
+ if (opts.sameSite) cookieString += `; SameSite=${opts.sameSite}`;
1685
+ if (opts.domain) cookieString += `; Domain=${opts.domain}`;
1686
+
1687
+ let existingHeaders = res.getHeader('Set-Cookie') || [];
1688
+ if (!Array.isArray(existingHeaders)) {
1689
+ existingHeaders = [existingHeaders];
1690
+ }
1691
+
1692
+ existingHeaders.push(cookieString);
1693
+ res.setHeader('Set-Cookie', existingHeaders);
1694
+
1695
+ return res;
1696
+ };
1697
+
1698
+ const originalEnd = res.end.bind(res);
1699
+
1700
+ res.end = (...args) => {
1701
+ const result = originalEnd(...args);
1702
+
1703
+ if (this.settings.debug && req._startTime) {
1704
+ const end = process.hrtime.bigint();
1705
+ const durationMs = Number(end - req._startTime) / 1_000_000;
1706
+
1707
+ this._debugLog(req, res, {
1708
+ time: durationMs
1709
+ });
1710
+ }
1711
+
1712
+ return result;
1713
+ };
1714
+ }
1715
+
1716
+ _defaultHtmlEngine(filePath, locals, callback) {
1717
+ fs.readFile(filePath, 'utf-8', (err, content) => {
1718
+ if (err) return callback(err);
1719
+
1720
+ let rendered = content;
1721
+
1722
+ Object.keys(locals).forEach(key => {
1723
+ if (locals[key] !== undefined && locals[key] !== null) {
1724
+ const safeRegex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
1725
+ const unsafeRegex = new RegExp(`{{{\\s*${key}\\s*}}}`, 'g');
1726
+
1727
+ if (safeRegex.test(rendered)) {
1728
+ const escaped = this._escapeHtml(String(locals[key]));
1729
+ rendered = rendered.replace(safeRegex, escaped);
1730
+ }
1731
+
1732
+ if (unsafeRegex.test(rendered)) {
1733
+ rendered = rendered.replace(unsafeRegex, String(locals[key]));
1734
+ }
1735
+ }
1736
+ });
1737
+
1738
+ callback(null, rendered);
1739
+ });
1740
+ }
1741
+
1742
+ _escapeHtml(text) {
1743
+ if (typeof text !== 'string') return text;
1744
+ return text
1745
+ .replace(/&/g, '&amp;')
1746
+ .replace(/</g, '&lt;')
1747
+ .replace(/>/g, '&gt;')
1748
+ .replace(/"/g, '&quot;')
1749
+ .replace(/'/g, '&#039;');
1750
+ }
1751
+
1752
+ async _runMiddleware(handler, req, res) {
1753
+ return new Promise((resolve, reject) => {
1754
+ const next = (err) => err ? reject(err) : resolve();
1755
+ const result = handler(req, res, next);
1756
+ if (result && typeof result.then === 'function') {
1757
+ result.then(resolve).catch(reject);
1758
+ }
1759
+ });
1760
+ }
1761
+
1762
+ _debugLog(req, res, meta) {
1763
+ if (!this.settings.debug) return;
1764
+
1765
+ let timeFormatted;
1766
+ const timeMs = meta.time;
1767
+
1768
+ if (timeMs < 1) {
1769
+ const us = (timeMs * 1000).toFixed(1);
1770
+ timeFormatted = `${us}µs`;
1771
+ } else if (timeMs >= 1000) {
1772
+ const s = (timeMs / 1000).toFixed(3);
1773
+ timeFormatted = `${s}s`;
1774
+ } else {
1775
+ timeFormatted = `${timeMs.toFixed(3)}ms`;
1776
+ }
1777
+
1778
+ const color = (code) =>
1779
+ code >= 500 ? '\x1b[31m' :
1780
+ code >= 400 ? '\x1b[33m' :
1781
+ code >= 300 ? '\x1b[36m' :
1782
+ '\x1b[32m';
1783
+
1784
+ const bodySize = req._bodySize || 0;
1785
+ let bodySizeFormatted;
1786
+
1787
+ if (bodySize === 0) {
1788
+ bodySizeFormatted = '0 bytes';
1789
+ } else if (bodySize < 1024) {
1790
+ bodySizeFormatted = `${bodySize} bytes`;
1791
+ } else if (bodySize < 1024 * 1024) {
1792
+ bodySizeFormatted = `${(bodySize / 1024).toFixed(2)} KB`;
1793
+ } else if (bodySize < 1024 * 1024 * 1024) {
1794
+ bodySizeFormatted = `${(bodySize / (1024 * 1024)).toFixed(2)} MB`;
1795
+ } else {
1796
+ bodySizeFormatted = `${(bodySize / (1024 * 1024 * 1024)).toFixed(2)} GB`;
1797
+ }
1798
+
1799
+ const logLines = [
1800
+ '[DEBUG REQUEST]',
1801
+ `→ ${req.method} ${req.originalUrl}`,
1802
+ `→ IP: ${req.ip.ipv4 || '127.0.0.1'}`,
1803
+ `→ Status: ${color(res.statusCode)}${res.statusCode}\x1b[0m`,
1804
+ `→ Duration: ${timeFormatted}`,
1805
+ ];
1806
+
1807
+ if (req.params && Object.keys(req.params).length > 0) {
1808
+ logLines.push(`→ Params: ${JSON.stringify(req.params)}`);
1809
+ }
1810
+
1811
+ if (req.query && Object.keys(req.query).length > 0) {
1812
+ logLines.push(`→ Query: ${JSON.stringify(req.query)}`);
1813
+ }
1814
+
1815
+ if (req.body && Object.keys(req.body).length > 0) {
1816
+ const bodyStr = JSON.stringify(req.body);
1817
+ const truncated = bodyStr.substring(0, 200) + (bodyStr.length > 200 ? '...' : '');
1818
+ logLines.push(`→ Body: ${truncated}`);
1819
+ logLines.push(`→ Body Size: ${bodySizeFormatted}`);
1820
+ }
1821
+
1822
+ if (req.files && Object.keys(req.files).length > 0) {
1823
+ logLines.push(`→ Files: ${Object.keys(req.files).join(', ')}`);
1824
+ }
1825
+
1826
+ logLines.push('---------------------------------------------');
1827
+ console.log('\n' + logLines.join('\n') + '\n');
1828
+ }
1829
+
1830
+ listRoutes() {
1831
+ const routeEntries = [];
1832
+
1833
+ this.routes.forEach(route => {
1834
+ const existing = routeEntries.find(
1835
+ entry => entry.method === route.method &&
1836
+ entry.handler === route.handler
1837
+ );
1838
+
1839
+ if (existing) {
1840
+ if (!Array.isArray(existing.path)) {
1841
+ existing.path = [existing.path];
1842
+ }
1843
+ existing.path.push(route.path);
1844
+ } else {
1845
+ routeEntries.push({
1846
+ method: route.method,
1847
+ path: route.path,
1848
+ middlewares: route.middlewares.length,
1849
+ handler: route.handler
1850
+ });
1851
+ }
1852
+ });
1853
+
1854
+ return routeEntries.map(entry => ({
1855
+ method: entry.method,
1856
+ path: entry.path,
1857
+ middlewares: entry.middlewares
1858
+ }));
1859
+ }
1860
+
1861
+ printRoutes() {
1862
+ if (this.routes.length === 0) {
1863
+ console.log('\nNo routes registered.\n');
1864
+ return;
1865
+ }
1866
+
1867
+ console.log(`\nRegistered Routes: ${this.routes.length}\n`);
1868
+
1869
+ const grouped = new Map();
1870
+
1871
+ for (const route of this.routes) {
1872
+ const key = `${route.method}|${route.handler}`;
1873
+ if (!grouped.has(key)) {
1874
+ grouped.set(key, {
1875
+ method: route.method,
1876
+ paths: [],
1877
+ mw: route.middlewares.length
1878
+ });
1879
+ }
1880
+ const entry = grouped.get(key);
1881
+ const p = route.path || '/';
1882
+ if (!entry.paths.includes(p)) {
1883
+ entry.paths.push(p);
1884
+ }
1885
+ }
1886
+
1887
+ const sorted = Array.from(grouped.values()).sort((a, b) => {
1888
+ if (a.method !== b.method) return a.method.localeCompare(b.method);
1889
+ return a.paths[0].localeCompare(b.paths[0]);
1890
+ });
1891
+
1892
+ for (const r of sorted) {
1893
+ const pathStr = r.paths.length === 1
1894
+ ? r.paths[0]
1895
+ : r.paths.join(', ');
1896
+
1897
+ console.log(` \x1b[36m${r.method.padEnd(7)}\x1b[0m \x1b[33m${pathStr}\x1b[0m \x1b[90m(mw: ${r.mw})\x1b[0m`);
1898
+ }
1899
+ }
1900
+
1901
+ listen() {
1902
+ const args = Array.from(arguments);
1903
+ const server = createServer(this._handleRequest.bind(this));
1904
+ server.listen.apply(server, args);
1905
+ this.server = server;
1906
+ return server;
1907
+ }
1908
+ }
1909
+
1910
+ function Lieko() {
1911
+ return new LiekoExpress();
1912
+ }
1913
+
1914
+ function Router() {
1915
+ return new Lieko();
1916
+ }
1917
+
1918
+ module.exports = Lieko;
1919
+ module.exports.Router = Router;
1920
+
1921
+ module.exports.Schema = Schema;
1922
+ module.exports.createSchema = (...args) => new Schema(...args);
1923
+ module.exports.validators = validators;
1924
+ module.exports.validate = validate;
1925
+ module.exports.validatePartial = validatePartial;
1926
+ module.exports.ValidationError = ValidationError;