lieko-express 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cors.js ADDED
@@ -0,0 +1,100 @@
1
+ module.exports = function cors(userOptions = {}) {
2
+ const defaultOptions = {
3
+ enabled: true,
4
+ origin: "*",
5
+ strictOrigin: false,
6
+ allowPrivateNetwork: false,
7
+ methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
8
+ headers: ["Content-Type", "Authorization"],
9
+ credentials: false,
10
+ maxAge: 86400,
11
+ exposedHeaders: [],
12
+ debug: false
13
+ };
14
+
15
+ const opts = { ...defaultOptions, ...userOptions, enabled: true };
16
+
17
+ const _matchOrigin = (origin, allowedOrigin) => {
18
+ if (!origin || !allowedOrigin) return false;
19
+ if (Array.isArray(allowedOrigin)) {
20
+ return allowedOrigin.some(o => _matchOrigin(origin, o));
21
+ }
22
+ if (allowedOrigin === "*") return true;
23
+ if (allowedOrigin.includes("*")) {
24
+ const regex = new RegExp("^" + allowedOrigin
25
+ .replace(/\./g, "\\.")
26
+ .replace(/\*/g, ".*") + "$");
27
+ return regex.test(origin);
28
+ }
29
+ return origin === allowedOrigin;
30
+ };
31
+
32
+ const _logDebug = (req, finalOpts) => {
33
+ if (!finalOpts.debug) return;
34
+ console.log("\n[CORS DEBUG]");
35
+ console.log("Request:", req.method, req.url);
36
+ console.log("Origin:", req.headers.origin || "none");
37
+ console.log("Applied CORS Policy:");
38
+ console.log(" - Access-Control-Allow-Origin:", finalOpts.origin);
39
+ console.log(" - Methods:", finalOpts.methods.join(", "));
40
+ console.log(" - Headers:", finalOpts.headers.join(", "));
41
+ if (finalOpts.credentials) console.log(" - Credentials: true");
42
+ if (finalOpts.exposedHeaders?.length) console.log(" - Exposed:", finalOpts.exposedHeaders.join(", "));
43
+ console.log(" - Max-Age:", finalOpts.maxAge);
44
+ if (req.method === "OPTIONS") console.log("Preflight handled → 204\n");
45
+ };
46
+
47
+ return function corsMiddleware(req, res, next) {
48
+ if (!opts.enabled) return next();
49
+
50
+ const requestOrigin = req.headers.origin || "";
51
+
52
+ let finalOrigin = "*";
53
+
54
+ if (opts.strictOrigin && requestOrigin) {
55
+ const allowed = _matchOrigin(requestOrigin, opts.origin);
56
+ if (!allowed) {
57
+ res.statusCode = 403;
58
+ return res.end(JSON.stringify({
59
+ success: false,
60
+ error: "Origin Forbidden",
61
+ message: `Origin "${requestOrigin}" is not allowed`
62
+ }));
63
+ }
64
+ }
65
+
66
+ if (opts.origin === "*") {
67
+ finalOrigin = "*";
68
+ } else if (Array.isArray(opts.origin)) {
69
+ const match = opts.origin.find(o => _matchOrigin(requestOrigin, o));
70
+ finalOrigin = match || opts.origin[0];
71
+ } else {
72
+ finalOrigin = _matchOrigin(requestOrigin, opts.origin) ? requestOrigin : opts.origin;
73
+ }
74
+
75
+ _logDebug(req, { ...opts, origin: finalOrigin });
76
+
77
+ res.setHeader("Access-Control-Allow-Origin", finalOrigin);
78
+
79
+ if (opts.credentials) {
80
+ res.setHeader("Access-Control-Allow-Credentials", "true");
81
+ }
82
+
83
+ if (opts.exposedHeaders?.length) {
84
+ res.setHeader("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
85
+ }
86
+
87
+ if (opts.allowPrivateNetwork && req.headers["access-control-request-private-network"] === "true") {
88
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
89
+ }
90
+
91
+ if (req.method === "OPTIONS") {
92
+ res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
93
+ res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
94
+ res.setHeader("Access-Control-Max-Age", opts.maxAge);
95
+ res.statusCode = 204;
96
+ return res.end();
97
+ }
98
+ next();
99
+ };
100
+ };
package/lib/schema.js ADDED
@@ -0,0 +1,418 @@
1
+ class ValidationError extends Error {
2
+ constructor(errors) {
3
+ super('Validation failed');
4
+ this.name = 'ValidationError';
5
+ this.errors = errors;
6
+ }
7
+ }
8
+
9
+ class Schema {
10
+ constructor(rules) {
11
+ this.rules = rules;
12
+ this.fields = rules;
13
+ }
14
+
15
+ validate(data) {
16
+ const errors = [];
17
+ for (const [field, validators] of Object.entries(this.rules)) {
18
+ const value = data[field];
19
+ for (const validator of validators) {
20
+ const error = validator(value, field, data);
21
+ if (error) {
22
+ errors.push(error);
23
+ break;
24
+ }
25
+ }
26
+ }
27
+ if (errors.length > 0) throw new ValidationError(errors);
28
+ return true;
29
+ }
30
+ }
31
+
32
+ const validators = {
33
+ required: (message = 'Field is required') => {
34
+ return (value, field) => {
35
+ if (value === undefined || value === null || value === '') {
36
+ return { field, message, type: 'required' };
37
+ }
38
+ return null;
39
+ };
40
+ },
41
+
42
+ requiredTrue: (message = 'Field must be true') => {
43
+ return (value, field) => {
44
+ const normalized = value === true || value === 'true' || value === '1' || value === 1;
45
+ if (!normalized) {
46
+ return { field, message, type: 'requiredTrue' };
47
+ }
48
+ return null;
49
+ }
50
+ },
51
+
52
+ optional: () => {
53
+ return () => null;
54
+ },
55
+
56
+ string: (message = 'Field must be a string') => {
57
+ return (value, field) => {
58
+ if (value !== undefined && typeof value !== 'string') {
59
+ return { field, message, type: 'string' };
60
+ }
61
+ return null;
62
+ };
63
+ },
64
+
65
+ number: (message = 'Field must be a number') => {
66
+ return (value, field) => {
67
+ if (value !== undefined && typeof value !== 'number') {
68
+ return { field, message, type: 'number' };
69
+ }
70
+ return null;
71
+ };
72
+ },
73
+
74
+ boolean: (message = 'Field must be a boolean') => {
75
+ return (value, field) => {
76
+ if (value === undefined || value === null || value === '') return null;
77
+
78
+ const validTrue = ['true', true, 1, '1'];
79
+ const validFalse = ['false', false, 0, '0'];
80
+
81
+ const isValid = validTrue.includes(value) || validFalse.includes(value);
82
+
83
+ if (!isValid) {
84
+ return { field, message, type: 'boolean' };
85
+ }
86
+
87
+ return null;
88
+ };
89
+ },
90
+
91
+ integer: (message = 'Field must be an integer') => {
92
+ return (value, field) => {
93
+ if (value !== undefined && !Number.isInteger(value)) {
94
+ return { field, message, type: 'integer' };
95
+ }
96
+ return null;
97
+ };
98
+ },
99
+
100
+ positive: (message = 'Field must be positive') => {
101
+ return (value, field) => {
102
+ if (value !== undefined && value <= 0) {
103
+ return { field, message, type: 'positive' };
104
+ }
105
+ return null;
106
+ };
107
+ },
108
+
109
+ negative: (message = 'Field must be negative') => {
110
+ return (value, field) => {
111
+ if (value !== undefined && value >= 0) {
112
+ return { field, message, type: 'negative' };
113
+ }
114
+ return null;
115
+ };
116
+ },
117
+
118
+ email: (message = 'Invalid email format') => {
119
+ return (value, field) => {
120
+ if (value !== undefined && value !== null) {
121
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
122
+ if (!emailRegex.test(value)) {
123
+ return { field, message, type: 'email' };
124
+ }
125
+ }
126
+ return null;
127
+ };
128
+ },
129
+
130
+ min: (minValue, message) => {
131
+ return (value, field) => {
132
+ if (value !== undefined && value !== null) {
133
+ if (typeof value === 'string' && value.length < minValue) {
134
+ return {
135
+ field,
136
+ message: message || `Field must be at least ${minValue} characters`,
137
+ type: 'min'
138
+ };
139
+ }
140
+ if (typeof value === 'number' && value < minValue) {
141
+ return {
142
+ field,
143
+ message: message || `Field must be at least ${minValue}`,
144
+ type: 'min'
145
+ };
146
+ }
147
+ }
148
+ return null;
149
+ };
150
+ },
151
+
152
+ max: (maxValue, message) => {
153
+ return (value, field) => {
154
+ if (value !== undefined && value !== null) {
155
+ if (typeof value === 'string' && value.length > maxValue) {
156
+ return {
157
+ field,
158
+ message: message || `Field must be at most ${maxValue} characters`,
159
+ type: 'max'
160
+ };
161
+ }
162
+ if (typeof value === 'number' && value > maxValue) {
163
+ return {
164
+ field,
165
+ message: message || `Field must be at most ${maxValue}`,
166
+ type: 'max'
167
+ };
168
+ }
169
+ }
170
+ return null;
171
+ };
172
+ },
173
+
174
+ length: (n, message) => {
175
+ return (value, field) => {
176
+ if (typeof value === 'string' && value.length !== n) {
177
+ return {
178
+ field,
179
+ message: message || `Field must be exactly ${n} characters`,
180
+ type: 'length'
181
+ };
182
+ }
183
+ return null;
184
+ };
185
+ },
186
+
187
+ minLength: (minLength, message) => {
188
+ return (value, field) => {
189
+ if (value !== undefined && value !== null && typeof value === 'string') {
190
+ if (value.length < minLength) {
191
+ return {
192
+ field,
193
+ message: message || `Field must be at least ${minLength} characters`,
194
+ type: 'minLength'
195
+ };
196
+ }
197
+ }
198
+ return null;
199
+ };
200
+ },
201
+
202
+ maxLength: (maxLength, message) => {
203
+ return (value, field) => {
204
+ if (value !== undefined && value !== null && typeof value === 'string') {
205
+ if (value.length > maxLength) {
206
+ return {
207
+ field,
208
+ message: message || `Field must be at most ${maxLength} characters`,
209
+ type: 'maxLength'
210
+ };
211
+ }
212
+ }
213
+ return null;
214
+ };
215
+ },
216
+
217
+ pattern: (regex, message = 'Invalid format') => {
218
+ return (value, field) => {
219
+ if (value !== undefined && value !== null && typeof value === 'string') {
220
+ if (!regex.test(value)) {
221
+ return { field, message, type: 'pattern' };
222
+ }
223
+ }
224
+ return null;
225
+ };
226
+ },
227
+
228
+ oneOf: (allowedValues, message) => {
229
+ return (value, field) => {
230
+ if (value !== undefined && value !== null) {
231
+ if (!allowedValues.includes(value)) {
232
+ return {
233
+ field,
234
+ message: message || `Field must be one of: ${allowedValues.join(', ')}`,
235
+ type: 'oneOf'
236
+ };
237
+ }
238
+ }
239
+ return null;
240
+ };
241
+ },
242
+
243
+ notOneOf: (values, message) => {
244
+ return (value, field) => {
245
+ if (values.includes(value)) {
246
+ return {
247
+ field,
248
+ message: message || `Field cannot be one of: ${values.join(', ')}`,
249
+ type: 'notOneOf'
250
+ };
251
+ }
252
+ return null;
253
+ };
254
+ },
255
+
256
+ custom: (validatorFn, message = 'Validation failed') => {
257
+ return (value, field, data) => {
258
+ const isValid = validatorFn(value, data);
259
+ if (!isValid) {
260
+ return { field, message, type: 'custom' };
261
+ }
262
+ return null;
263
+ };
264
+ },
265
+
266
+ equal: (expectedValue, message) => {
267
+ return (value, field) => {
268
+ if (value !== expectedValue) {
269
+ return {
270
+ field,
271
+ message: message || `Field must be equal to ${expectedValue}`,
272
+ type: 'equal'
273
+ };
274
+ }
275
+ return null;
276
+ };
277
+ },
278
+
279
+ mustBeTrue: (message = 'This field must be accepted') => {
280
+ return (value, field) => {
281
+ const normalized = value === true || value === 'true' || value === '1' || value === 1;
282
+ if (!normalized) {
283
+ return { field, message, type: 'mustBeTrue' };
284
+ }
285
+ return null;
286
+ };
287
+ },
288
+
289
+ mustBeFalse: (message = 'This field must be declined') => {
290
+ return (value, field) => {
291
+ const normalized = value === false || value === 'false' || value === '0' || value === 0;
292
+ if (!normalized) {
293
+ return { field, message, type: 'mustBeFalse' };
294
+ }
295
+ return null;
296
+ };
297
+ },
298
+
299
+ date: (message = 'Invalid date') => {
300
+ return (value, field) => {
301
+ if (!value) return null;
302
+ const date = new Date(value);
303
+ if (isNaN(date.getTime())) {
304
+ return { field, message, type: 'date' };
305
+ }
306
+ return null;
307
+ };
308
+ },
309
+
310
+ before: (limit, message) => {
311
+ return (value, field) => {
312
+ if (!value) return null;
313
+ const d1 = new Date(value);
314
+ const d2 = new Date(limit);
315
+ if (isNaN(d1) || d1 >= d2) {
316
+ return {
317
+ field,
318
+ message: message || `Date must be before ${limit}`,
319
+ type: 'before'
320
+ };
321
+ }
322
+ return null;
323
+ };
324
+ },
325
+
326
+ after: (limit, message) => {
327
+ return (value, field) => {
328
+ if (!value) return null;
329
+ const d1 = new Date(value);
330
+ const d2 = new Date(limit);
331
+ if (isNaN(d1) || d1 <= d2) {
332
+ return {
333
+ field,
334
+ message: message || `Date must be after ${limit}`,
335
+ type: 'after'
336
+ };
337
+ }
338
+ return null;
339
+ };
340
+ },
341
+
342
+ startsWith: (prefix, message) => {
343
+ return (value, field) => {
344
+ if (typeof value === 'string' && !value.startsWith(prefix)) {
345
+ return {
346
+ field,
347
+ message: message || `Field must start with "${prefix}"`,
348
+ type: 'startsWith'
349
+ };
350
+ }
351
+ return null;
352
+ };
353
+ },
354
+
355
+ endsWith: (suffix, message) => {
356
+ return (value, field) => {
357
+ if (typeof value === 'string' && !value.endsWith(suffix)) {
358
+ return {
359
+ field,
360
+ message: message || `Field must end with "${suffix}"`,
361
+ type: 'endsWith'
362
+ };
363
+ }
364
+ return null;
365
+ };
366
+ }
367
+ };
368
+
369
+ function validate(schema) {
370
+ return (req, res, next) => {
371
+ try {
372
+ schema.validate(req.body);
373
+ next();
374
+ } catch (error) {
375
+ if (error instanceof ValidationError) {
376
+ return res.status(400).json({
377
+ success: false,
378
+ message: 'Validation failed',
379
+ errors: error.errors
380
+ });
381
+ }
382
+ throw error;
383
+ }
384
+ };
385
+ }
386
+
387
+ function validatePartial(schema) {
388
+ const partial = {};
389
+
390
+ for (const field in schema.fields) {
391
+ const rules = schema.fields[field];
392
+
393
+ const filtered = rules.filter(v =>
394
+ v.name !== 'required' &&
395
+ v.name !== 'requiredTrue' &&
396
+ v.name !== 'mustBeTrue'
397
+ );
398
+ partial[field] = [validators.optional(), ...filtered];
399
+ }
400
+
401
+ return new Schema(partial);
402
+ }
403
+
404
+ module.exports = {
405
+ Schema,
406
+ ValidationError,
407
+ validators,
408
+ validate,
409
+ validatePartial,
410
+
411
+ schema: (...args) => new Schema(...args),
412
+ v: validators,
413
+ validator: validators,
414
+ middleware: {
415
+ validate,
416
+ validatePartial
417
+ }
418
+ };
package/lib/static.js ADDED
@@ -0,0 +1,247 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ module.exports = function serveStatic(root, options = {}) {
5
+ const opts = {
6
+ maxAge: options.maxAge || 0,
7
+ index: options.index !== undefined ? options.index : 'index.html',
8
+ dotfiles: options.dotfiles || 'ignore',
9
+ etag: options.etag !== undefined ? options.etag : true,
10
+ extensions: options.extensions || false,
11
+ fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
12
+ immutable: options.immutable || false,
13
+ lastModified: options.lastModified !== undefined ? options.lastModified : true,
14
+ redirect: options.redirect !== undefined ? options.redirect : true,
15
+ setHeaders: options.setHeaders || null,
16
+ cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
17
+ };
18
+
19
+ const mimeTypes = {
20
+ '.html': 'text/html; charset=utf-8',
21
+ '.htm': 'text/html; charset=utf-8',
22
+ '.css': 'text/css; charset=utf-8',
23
+ '.js': 'application/javascript; charset=utf-8',
24
+ '.mjs': 'application/javascript; charset=utf-8',
25
+ '.json': 'application/json; charset=utf-8',
26
+ '.xml': 'application/xml; charset=utf-8',
27
+ '.txt': 'text/plain; charset=utf-8',
28
+ '.md': 'text/markdown; charset=utf-8',
29
+ '.jpg': 'image/jpeg',
30
+ '.jpeg': 'image/jpeg',
31
+ '.png': 'image/png',
32
+ '.gif': 'image/gif',
33
+ '.svg': 'image/svg+xml',
34
+ '.webp': 'image/webp',
35
+ '.ico': 'image/x-icon',
36
+ '.bmp': 'image/bmp',
37
+ '.tiff': 'image/tiff',
38
+ '.tif': 'image/tiff',
39
+ '.mp3': 'audio/mpeg',
40
+ '.wav': 'audio/wav',
41
+ '.ogg': 'audio/ogg',
42
+ '.m4a': 'audio/mp4',
43
+ '.aac': 'audio/aac',
44
+ '.flac': 'audio/flac',
45
+ '.mp4': 'video/mp4',
46
+ '.webm': 'video/webm',
47
+ '.ogv': 'video/ogg',
48
+ '.avi': 'video/x-msvideo',
49
+ '.mov': 'video/quicktime',
50
+ '.wmv': 'video/x-ms-wmv',
51
+ '.flv': 'video/x-flv',
52
+ '.mkv': 'video/x-matroska',
53
+ '.woff': 'font/woff',
54
+ '.woff2': 'font/woff2',
55
+ '.ttf': 'font/ttf',
56
+ '.otf': 'font/otf',
57
+ '.eot': 'application/vnd.ms-fontobject',
58
+ '.zip': 'application/zip',
59
+ '.rar': 'application/x-rar-compressed',
60
+ '.tar': 'application/x-tar',
61
+ '.gz': 'application/gzip',
62
+ '.7z': 'application/x-7z-compressed',
63
+ '.pdf': 'application/pdf',
64
+ '.doc': 'application/msword',
65
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
66
+ '.xls': 'application/vnd.ms-excel',
67
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
68
+ '.ppt': 'application/vnd.ms-powerpoint',
69
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
70
+ '.wasm': 'application/wasm',
71
+ '.csv': 'text/csv; charset=utf-8'
72
+ };
73
+
74
+ const getMimeType = (filePath) => {
75
+ const ext = path.extname(filePath).toLowerCase();
76
+ return mimeTypes[ext] || 'application/octet-stream';
77
+ };
78
+
79
+ const generateETag = (stats) => {
80
+ const mtime = stats.mtime.getTime().toString(16);
81
+ const size = stats.size.toString(16);
82
+ return `W/"${size}-${mtime}"`;
83
+ };
84
+
85
+ return async (req, res, next) => {
86
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
87
+ return next();
88
+ }
89
+
90
+ try {
91
+ let pathname = req.url;
92
+ const qIndex = pathname.indexOf('?');
93
+ if (qIndex !== -1) {
94
+ pathname = pathname.substring(0, qIndex);
95
+ }
96
+
97
+ if (pathname === '') {
98
+ pathname = '/';
99
+ }
100
+
101
+ try {
102
+ pathname = decodeURIComponent(pathname);
103
+ } catch (e) {
104
+ if (opts.fallthrough) return next();
105
+ return res.status(400).send('Bad Request');
106
+ }
107
+
108
+ let filePath = pathname === '/' ? root : path.join(root, pathname);
109
+
110
+ const resolvedPath = path.resolve(filePath);
111
+ const resolvedRoot = path.resolve(root);
112
+
113
+ if (!resolvedPath.startsWith(resolvedRoot)) {
114
+ if (opts.fallthrough) return next();
115
+ return res.status(403).send('Forbidden');
116
+ }
117
+
118
+ let stats;
119
+ try {
120
+ stats = await fs.promises.stat(filePath);
121
+ } catch (err) {
122
+ if (pathname === '/' && opts.index) {
123
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
124
+ for (const indexFile of indexes) {
125
+ const indexPath = path.join(root, indexFile);
126
+ try {
127
+ stats = await fs.promises.stat(indexPath);
128
+ if (stats.isFile()) {
129
+ filePath = indexPath;
130
+ break;
131
+ }
132
+ } catch (e) { }
133
+ }
134
+ }
135
+
136
+ if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
137
+ let found = false;
138
+ for (const ext of opts.extensions) {
139
+ const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
140
+ try {
141
+ stats = await fs.promises.stat(testPath);
142
+ filePath = testPath;
143
+ found = true;
144
+ break;
145
+ } catch (e) { }
146
+ }
147
+ if (!found) return next();
148
+ } else if (!stats) {
149
+ return next();
150
+ }
151
+ }
152
+
153
+ if (stats.isDirectory()) {
154
+ if (opts.redirect && !pathname.endsWith('/')) {
155
+ const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
156
+ const redirectUrl = pathname + '/' + query;
157
+ return res.redirect(redirectUrl, 301);
158
+ }
159
+
160
+ if (opts.index) {
161
+ const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
162
+
163
+ for (const indexFile of indexes) {
164
+ const indexPath = path.join(filePath, indexFile);
165
+ try {
166
+ const indexStats = await fs.promises.stat(indexPath);
167
+ if (indexStats.isFile()) {
168
+ filePath = indexPath;
169
+ stats = indexStats;
170
+ break;
171
+ }
172
+ } catch (e) { }
173
+ }
174
+
175
+ if (stats.isDirectory()) {
176
+ if (opts.fallthrough) return next();
177
+ return res.status(404).send('Not Found');
178
+ }
179
+ } else {
180
+ if (opts.fallthrough) return next();
181
+ return res.status(404).send('Not Found');
182
+ }
183
+ }
184
+
185
+ if (opts.etag) {
186
+ const etag = generateETag(stats);
187
+ const ifNoneMatch = req.headers['if-none-match'];
188
+
189
+ if (ifNoneMatch === etag) {
190
+ res.statusCode = 304;
191
+ res.end();
192
+ return;
193
+ }
194
+
195
+ res.setHeader('ETag', etag);
196
+ }
197
+
198
+ if (opts.lastModified) {
199
+ const lastModified = stats.mtime.toUTCString();
200
+ const ifModifiedSince = req.headers['if-modified-since'];
201
+
202
+ if (ifModifiedSince === lastModified) {
203
+ res.statusCode = 304;
204
+ res.end();
205
+ return;
206
+ }
207
+
208
+ res.setHeader('Last-Modified', lastModified);
209
+ }
210
+
211
+ if (opts.cacheControl) {
212
+ let cacheControl = 'public';
213
+
214
+ if (opts.maxAge > 0) {
215
+ cacheControl += `, max-age=${opts.maxAge}`;
216
+ }
217
+
218
+ if (opts.immutable) {
219
+ cacheControl += ', immutable';
220
+ }
221
+
222
+ res.setHeader('Cache-Control', cacheControl);
223
+ }
224
+
225
+ const mimeType = getMimeType(filePath);
226
+ res.setHeader('Content-Type', mimeType);
227
+ res.setHeader('Content-Length', stats.size);
228
+
229
+ if (typeof opts.setHeaders === 'function') {
230
+ opts.setHeaders(res, filePath, stats);
231
+ }
232
+
233
+ if (req.method === 'HEAD') {
234
+ return res.end();
235
+ }
236
+
237
+ const data = await fs.promises.readFile(filePath);
238
+ res.end(data);
239
+ return;
240
+
241
+ } catch (error) {
242
+ console.error('Static middleware error:', error);
243
+ if (opts.fallthrough) return next();
244
+ res.status(500).send('Internal Server Error');
245
+ }
246
+ };
247
+ };
package/lieko-express.js CHANGED
@@ -64,124 +64,20 @@ class LiekoExpress {
64
64
  }
65
65
 
66
66
  cors(options = {}) {
67
- this.corsOptions = {
68
- ...this.corsOptions,
69
- enabled: true,
70
- ...options
71
- };
72
- }
73
-
74
- _matchOrigin(origin, allowedOrigin) {
75
- if (!origin || !allowedOrigin) return false;
76
-
77
- if (Array.isArray(allowedOrigin)) {
78
- return allowedOrigin.some(o => this._matchOrigin(origin, o));
79
- }
80
-
81
- if (allowedOrigin === "*") return true;
82
-
83
- // Wildcard https://*.example.com
84
- if (allowedOrigin.includes("*")) {
85
- const regex = new RegExp("^" + allowedOrigin
86
- .replace(/\./g, "\\.")
87
- .replace(/\*/g, ".*") + "$");
88
-
89
- return regex.test(origin);
90
- }
91
-
92
- return origin === allowedOrigin;
93
- }
94
-
95
- _applyCors(req, res, opts) {
96
- if (!opts || !opts.enabled) return;
97
-
98
- const requestOrigin = req.headers.origin || "";
99
-
100
- let finalOrigin = "*";
101
-
102
- if (opts.strictOrigin && requestOrigin) {
103
- const allowed = this._matchOrigin(requestOrigin, opts.origin);
104
-
105
- if (!allowed) {
106
- res.statusCode = 403;
107
- return res.end(JSON.stringify({
108
- success: false,
109
- error: "Origin Forbidden",
110
- message: `Origin "${requestOrigin}" is not allowed`
111
- }));
112
- }
113
- }
114
-
115
- if (opts.origin === "*") {
116
- finalOrigin = "*";
117
- } else if (Array.isArray(opts.origin)) {
118
- const match = opts.origin.find(o => this._matchOrigin(requestOrigin, o));
119
- finalOrigin = match || opts.origin[0];
120
- } else {
121
- finalOrigin = this._matchOrigin(requestOrigin, opts.origin)
122
- ? requestOrigin
123
- : opts.origin;
67
+ if (options === false) {
68
+ this._corsMiddleware = null;
69
+ return this;
124
70
  }
125
71
 
126
- this._logCorsDebug(req, {
127
- ...opts,
128
- origin: finalOrigin
129
- });
72
+ const middleware = require('./lib/cors')(options);
73
+ this._corsMiddleware = middleware;
130
74
 
131
- res.setHeader("Access-Control-Allow-Origin", finalOrigin);
132
-
133
- if (opts.credentials) {
134
- res.setHeader("Access-Control-Allow-Credentials", "true");
135
- }
136
-
137
- if (opts.exposedHeaders?.length) {
138
- res.setHeader("Access-Control-Expose-Headers",
139
- opts.exposedHeaders.join(", "));
75
+ const stack = new Error().stack;
76
+ if (!stack.includes('at LiekoExpress.use')) {
77
+ this.use(middleware);
140
78
  }
141
79
 
142
- // Chrome Private Network Access
143
- if (
144
- opts.allowPrivateNetwork &&
145
- req.headers["access-control-request-private-network"] === "true"
146
- ) {
147
- res.setHeader("Access-Control-Allow-Private-Network", "true");
148
- }
149
-
150
- if (req.method === "OPTIONS") {
151
- res.setHeader("Access-Control-Allow-Methods", opts.methods.join(", "));
152
- res.setHeader("Access-Control-Allow-Headers", opts.headers.join(", "));
153
- res.setHeader("Access-Control-Max-Age", opts.maxAge);
154
-
155
- res.statusCode = 204;
156
- return res.end();
157
- }
158
- }
159
-
160
- _logCorsDebug(req, opts) {
161
- if (!opts.debug) return;
162
-
163
- console.log("\n[CORS DEBUG]");
164
- console.log("Request:", req.method, req.url);
165
- console.log("Origin:", req.headers.origin || "none");
166
-
167
- console.log("Applied CORS Policy:");
168
- console.log(" - Access-Control-Allow-Origin:", opts.origin);
169
- console.log(" - Access-Control-Allow-Methods:", opts.methods.join(", "));
170
- console.log(" - Access-Control-Allow-Headers:", opts.headers.join(", "));
171
-
172
- if (opts.credentials) {
173
- console.log(" - Access-Control-Allow-Credentials: true");
174
- }
175
-
176
- if (opts.exposedHeaders?.length) {
177
- console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
178
- }
179
-
180
- console.log(" - Max-Age:", opts.maxAge);
181
-
182
- if (req.method === "OPTIONS") {
183
- console.log("Preflight request handled with status 204\n");
184
- }
80
+ return middleware;
185
81
  }
186
82
 
187
83
  debug(value = true) {
@@ -708,248 +604,7 @@ ${cyan} (req, res, next) => {
708
604
  }
709
605
 
710
606
  static(root, options = {}) {
711
- const opts = {
712
- maxAge: options.maxAge || 0,
713
- index: options.index !== undefined ? options.index : 'index.html',
714
- dotfiles: options.dotfiles || 'ignore',
715
- etag: options.etag !== undefined ? options.etag : true,
716
- extensions: options.extensions || false,
717
- fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
718
- immutable: options.immutable || false,
719
- lastModified: options.lastModified !== undefined ? options.lastModified : true,
720
- redirect: options.redirect !== undefined ? options.redirect : true,
721
- setHeaders: options.setHeaders || null,
722
- cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
723
- };
724
-
725
- const mimeTypes = {
726
- '.html': 'text/html; charset=utf-8',
727
- '.htm': 'text/html; charset=utf-8',
728
- '.css': 'text/css; charset=utf-8',
729
- '.js': 'application/javascript; charset=utf-8',
730
- '.mjs': 'application/javascript; charset=utf-8',
731
- '.json': 'application/json; charset=utf-8',
732
- '.xml': 'application/xml; charset=utf-8',
733
- '.txt': 'text/plain; charset=utf-8',
734
- '.md': 'text/markdown; charset=utf-8',
735
- '.jpg': 'image/jpeg',
736
- '.jpeg': 'image/jpeg',
737
- '.png': 'image/png',
738
- '.gif': 'image/gif',
739
- '.svg': 'image/svg+xml',
740
- '.webp': 'image/webp',
741
- '.ico': 'image/x-icon',
742
- '.bmp': 'image/bmp',
743
- '.tiff': 'image/tiff',
744
- '.tif': 'image/tiff',
745
- '.mp3': 'audio/mpeg',
746
- '.wav': 'audio/wav',
747
- '.ogg': 'audio/ogg',
748
- '.m4a': 'audio/mp4',
749
- '.aac': 'audio/aac',
750
- '.flac': 'audio/flac',
751
- '.mp4': 'video/mp4',
752
- '.webm': 'video/webm',
753
- '.ogv': 'video/ogg',
754
- '.avi': 'video/x-msvideo',
755
- '.mov': 'video/quicktime',
756
- '.wmv': 'video/x-ms-wmv',
757
- '.flv': 'video/x-flv',
758
- '.mkv': 'video/x-matroska',
759
- '.woff': 'font/woff',
760
- '.woff2': 'font/woff2',
761
- '.ttf': 'font/ttf',
762
- '.otf': 'font/otf',
763
- '.eot': 'application/vnd.ms-fontobject',
764
- '.zip': 'application/zip',
765
- '.rar': 'application/x-rar-compressed',
766
- '.tar': 'application/x-tar',
767
- '.gz': 'application/gzip',
768
- '.7z': 'application/x-7z-compressed',
769
- '.pdf': 'application/pdf',
770
- '.doc': 'application/msword',
771
- '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
772
- '.xls': 'application/vnd.ms-excel',
773
- '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
774
- '.ppt': 'application/vnd.ms-powerpoint',
775
- '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
776
- '.wasm': 'application/wasm',
777
- '.csv': 'text/csv; charset=utf-8'
778
- };
779
-
780
- const getMimeType = (filePath) => {
781
- const ext = path.extname(filePath).toLowerCase();
782
- return mimeTypes[ext] || 'application/octet-stream';
783
- };
784
-
785
- const generateETag = (stats) => {
786
- const mtime = stats.mtime.getTime().toString(16);
787
- const size = stats.size.toString(16);
788
- return `W/"${size}-${mtime}"`;
789
- };
790
-
791
- return async (req, res, next) => {
792
- if (req.method !== 'GET' && req.method !== 'HEAD') {
793
- return next();
794
- }
795
-
796
- try {
797
- let pathname = req.url;
798
- const qIndex = pathname.indexOf('?');
799
- if (qIndex !== -1) {
800
- pathname = pathname.substring(0, qIndex);
801
- }
802
-
803
- if (pathname === '') {
804
- pathname = '/';
805
- }
806
-
807
- try {
808
- pathname = decodeURIComponent(pathname);
809
- } catch (e) {
810
- if (opts.fallthrough) return next();
811
- return res.status(400).send('Bad Request');
812
- }
813
-
814
- let filePath = pathname === '/' ? root : path.join(root, pathname);
815
-
816
- const resolvedPath = path.resolve(filePath);
817
- const resolvedRoot = path.resolve(root);
818
-
819
- if (!resolvedPath.startsWith(resolvedRoot)) {
820
- if (opts.fallthrough) return next();
821
- return res.status(403).send('Forbidden');
822
- }
823
-
824
- let stats;
825
- try {
826
- stats = await fs.promises.stat(filePath);
827
- } catch (err) {
828
- if (pathname === '/' && opts.index) {
829
- const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
830
- for (const indexFile of indexes) {
831
- const indexPath = path.join(root, indexFile);
832
- try {
833
- stats = await fs.promises.stat(indexPath);
834
- if (stats.isFile()) {
835
- filePath = indexPath;
836
- break;
837
- }
838
- } catch (e) { }
839
- }
840
- }
841
-
842
- if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
843
- let found = false;
844
- for (const ext of opts.extensions) {
845
- const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
846
- try {
847
- stats = await fs.promises.stat(testPath);
848
- filePath = testPath;
849
- found = true;
850
- break;
851
- } catch (e) { }
852
- }
853
- if (!found) return next();
854
- } else if (!stats) {
855
- return next();
856
- }
857
- }
858
-
859
- if (stats.isDirectory()) {
860
- if (opts.redirect && !pathname.endsWith('/')) {
861
- const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
862
- const redirectUrl = pathname + '/' + query;
863
- return res.redirect(redirectUrl, 301);
864
- }
865
-
866
- if (opts.index) {
867
- const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
868
-
869
- for (const indexFile of indexes) {
870
- const indexPath = path.join(filePath, indexFile);
871
- try {
872
- const indexStats = await fs.promises.stat(indexPath);
873
- if (indexStats.isFile()) {
874
- filePath = indexPath;
875
- stats = indexStats;
876
- break;
877
- }
878
- } catch (e) { }
879
- }
880
-
881
- if (stats.isDirectory()) {
882
- if (opts.fallthrough) return next();
883
- return res.status(404).send('Not Found');
884
- }
885
- } else {
886
- if (opts.fallthrough) return next();
887
- return res.status(404).send('Not Found');
888
- }
889
- }
890
-
891
- if (opts.etag) {
892
- const etag = generateETag(stats);
893
- const ifNoneMatch = req.headers['if-none-match'];
894
-
895
- if (ifNoneMatch === etag) {
896
- res.statusCode = 304;
897
- res.end();
898
- return;
899
- }
900
-
901
- res.setHeader('ETag', etag);
902
- }
903
-
904
- if (opts.lastModified) {
905
- const lastModified = stats.mtime.toUTCString();
906
- const ifModifiedSince = req.headers['if-modified-since'];
907
-
908
- if (ifModifiedSince === lastModified) {
909
- res.statusCode = 304;
910
- res.end();
911
- return;
912
- }
913
-
914
- res.setHeader('Last-Modified', lastModified);
915
- }
916
-
917
- if (opts.cacheControl) {
918
- let cacheControl = 'public';
919
-
920
- if (opts.maxAge > 0) {
921
- cacheControl += `, max-age=${opts.maxAge}`;
922
- }
923
-
924
- if (opts.immutable) {
925
- cacheControl += ', immutable';
926
- }
927
-
928
- res.setHeader('Cache-Control', cacheControl);
929
- }
930
-
931
- const mimeType = getMimeType(filePath);
932
- res.setHeader('Content-Type', mimeType);
933
- res.setHeader('Content-Length', stats.size);
934
-
935
- if (typeof opts.setHeaders === 'function') {
936
- opts.setHeaders(res, filePath, stats);
937
- }
938
-
939
- if (req.method === 'HEAD') {
940
- return res.end();
941
- }
942
-
943
- const data = await fs.promises.readFile(filePath);
944
- res.end(data);
945
- return;
946
-
947
- } catch (error) {
948
- console.error('Static middleware error:', error);
949
- if (opts.fallthrough) return next();
950
- res.status(500).send('Internal Server Error');
951
- }
952
- };
607
+ return require('./lib/static')(root, options);
953
608
  }
954
609
 
955
610
  _mountRouter(basePath, router) {
@@ -1426,6 +1081,90 @@ ${cyan} (req, res, next) => {
1426
1081
  return undefined;
1427
1082
  };
1428
1083
  req.header = req.get;
1084
+
1085
+ const parseAccept = (header) => {
1086
+ if (!header) return [];
1087
+ return header
1088
+ .split(',')
1089
+ .map(part => {
1090
+ const [type, ...rest] = part.trim().split(';');
1091
+ const q = rest
1092
+ .find(p => p.trim().startsWith('q='))
1093
+ ?.split('=')[1];
1094
+ const quality = q ? parseFloat(q) : 1.0;
1095
+ return { type: type.trim().toLowerCase(), quality };
1096
+ })
1097
+ .filter(item => item.quality > 0)
1098
+ .sort((a, b) => b.quality - a.quality)
1099
+ .map(item => item.type);
1100
+ };
1101
+
1102
+ const accepts = (types) => {
1103
+ if (!Array.isArray(types)) types = [types];
1104
+ const accepted = parseAccept(req.headers['accept']);
1105
+
1106
+ for (const type of types) {
1107
+ const t = type.toLowerCase();
1108
+
1109
+ if (accepted.includes(t)) return type;
1110
+
1111
+ if (accepted.some(a => {
1112
+ if (a === '*/*') return true;
1113
+ if (a.endsWith('/*')) {
1114
+ const prefix = a.slice(0, -1);
1115
+ return t.startsWith(prefix);
1116
+ }
1117
+ return false;
1118
+ })) {
1119
+ return type;
1120
+ }
1121
+ }
1122
+
1123
+ return false;
1124
+ };
1125
+
1126
+ req.accepts = function (types) {
1127
+ return accepts(types);
1128
+ };
1129
+
1130
+ req.acceptsLanguages = function (langs) {
1131
+ if (!Array.isArray(langs)) langs = [langs];
1132
+ const accepted = parseAccept(req.headers['accept-language'] || '');
1133
+ for (const lang of langs) {
1134
+ const l = lang.toLowerCase();
1135
+ if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
1136
+ }
1137
+ return false;
1138
+ };
1139
+
1140
+ req.acceptsEncodings = function (encodings) {
1141
+ if (!Array.isArray(encodings)) encodings = [encodings];
1142
+ const accepted = parseAccept(req.headers['accept-encoding'] || '');
1143
+ for (const enc of encodings) {
1144
+ if (accepted.includes(enc.toLowerCase())) return enc;
1145
+ }
1146
+ return false;
1147
+ };
1148
+
1149
+ req.acceptsCharsets = function (charsets) {
1150
+ if (!Array.isArray(charsets)) charsets = [charsets];
1151
+ const accepted = parseAccept(req.headers['accept-charset'] || '');
1152
+ for (const charset of charsets) {
1153
+ if (accepted.includes(charset.toLowerCase())) return charset;
1154
+ }
1155
+ return false;
1156
+ };
1157
+
1158
+ req.is = function (type) {
1159
+ const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
1160
+ if (!type) return ct;
1161
+ const t = type.toLowerCase();
1162
+ if (t.includes('/')) return ct === t;
1163
+ if (t === 'json') return ct.includes('json');
1164
+ if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
1165
+ if (t === 'multipart') return ct.includes('multipart');
1166
+ return false;
1167
+ };
1429
1168
  }
1430
1169
 
1431
1170
  _enhanceResponse(req, res) {
@@ -1504,6 +1243,7 @@ ${cyan} (req, res, next) => {
1504
1243
  const locals = { ...res.locals, ...options };
1505
1244
  let viewPath = view;
1506
1245
  let ext = path.extname(view);
1246
+ console.log("EXT: ", ext)
1507
1247
 
1508
1248
  if (!ext) {
1509
1249
  ext = this.settings['view engine'];
@@ -1518,11 +1258,13 @@ ${cyan} (req, res, next) => {
1518
1258
 
1519
1259
  const viewsDir = this.settings.views || path.join(process.cwd(), 'views');
1520
1260
  let fullPath = path.join(viewsDir, viewPath);
1261
+ console.log(fullPath)
1521
1262
  let fileExists = false;
1522
1263
  try {
1523
1264
  await fs.promises.access(fullPath);
1524
1265
  fileExists = true;
1525
1266
  } catch (err) {
1267
+ console.log(err)
1526
1268
  const extensions = ['.html', '.ejs', '.pug', '.hbs'];
1527
1269
  for (const tryExt of extensions) {
1528
1270
  if (tryExt === ext) continue;
@@ -1841,7 +1583,7 @@ ${cyan} (req, res, next) => {
1841
1583
  logLines.push('---------------------------------------------');
1842
1584
  console.log('\n' + logLines.join('\n') + '\n');
1843
1585
  }
1844
-
1586
+
1845
1587
  listRoutes() {
1846
1588
  const routeEntries = [];
1847
1589
 
@@ -1916,7 +1658,6 @@ ${cyan} (req, res, next) => {
1916
1658
  listen() {
1917
1659
  const args = Array.from(arguments);
1918
1660
  const server = createServer(this._handleRequest.bind(this));
1919
-
1920
1661
  server.listen.apply(server, args);
1921
1662
  this.server = server;
1922
1663
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lieko-express",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/eiwSrvt/lieko-express"
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "lieko-express.js",
20
20
  "lieko-express.d.ts",
21
- "README.md"
21
+ "README.md",
22
+ "lib"
22
23
  ]
23
24
  }