lieko-express 0.0.8 → 0.0.10

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
@@ -61,127 +61,53 @@ class LiekoExpress {
61
61
  exposedHeaders: [],
62
62
  debug: false
63
63
  };
64
- }
65
-
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
64
 
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;
65
+ this.excludedPatterns = [
66
+ /^\/\.well-known\/.*/i // Chrome DevTools, Apple, etc.
67
+ ]
93
68
  }
94
69
 
95
- _applyCors(req, res, opts) {
96
- if (!opts || !opts.enabled) return;
97
-
98
- const requestOrigin = req.headers.origin || "";
70
+ excludeUrl(patterns) {
71
+ if (!Array.isArray(patterns)) patterns = [patterns];
72
+ this.excludedPatterns = this.excludedPatterns || [];
99
73
 
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
- }));
74
+ patterns.forEach(pattern => {
75
+ if (pattern instanceof RegExp) {
76
+ this.excludedPatterns.push(pattern);
77
+ return;
112
78
  }
113
- }
114
79
 
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;
124
- }
80
+ let regexStr = pattern
81
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
82
+ .replace(/\\\*/g, '.*');
125
83
 
126
- this._logCorsDebug(req, {
127
- ...opts,
128
- origin: finalOrigin
84
+ regexStr = '^' + regexStr + '$';
85
+ this.excludedPatterns.push(new RegExp(regexStr, 'i'));
129
86
  });
130
87
 
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(", "));
140
- }
141
-
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
- }
88
+ return this;
158
89
  }
159
90
 
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
- }
91
+ _isExcluded(url) {
92
+ if (!this.excludedPatterns?.length) return false;
93
+ return this.excludedPatterns.some(re => re.test(url));
94
+ }
175
95
 
176
- if (opts.exposedHeaders?.length) {
177
- console.log(" - Access-Control-Expose-Headers:", opts.exposedHeaders.join(", "));
96
+ cors(options = {}) {
97
+ if (options === false) {
98
+ this._corsMiddleware = null;
99
+ return this;
178
100
  }
179
101
 
180
- console.log(" - Max-Age:", opts.maxAge);
102
+ const middleware = require('./lib/cors')(options);
103
+ this._corsMiddleware = middleware;
181
104
 
182
- if (req.method === "OPTIONS") {
183
- console.log("Preflight request handled with status 204\n");
105
+ const stack = new Error().stack;
106
+ if (!stack.includes('at LiekoExpress.use')) {
107
+ this.use(middleware);
184
108
  }
109
+
110
+ return middleware;
185
111
  }
186
112
 
187
113
  debug(value = true) {
@@ -708,248 +634,7 @@ ${cyan} (req, res, next) => {
708
634
  }
709
635
 
710
636
  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
- };
637
+ return require('./lib/static')(root, options);
953
638
  }
954
639
 
955
640
  _mountRouter(basePath, router) {
@@ -1226,6 +911,10 @@ ${cyan} (req, res, next) => {
1226
911
  }
1227
912
 
1228
913
  async _handleRequest(req, res) {
914
+ if (this._isExcluded(req.url.split('?')[0])) {
915
+ return res.status(404).end();
916
+ }
917
+
1229
918
  this._enhanceRequest(req);
1230
919
 
1231
920
  const url = req.url;
@@ -1426,6 +1115,90 @@ ${cyan} (req, res, next) => {
1426
1115
  return undefined;
1427
1116
  };
1428
1117
  req.header = req.get;
1118
+
1119
+ const parseAccept = (header) => {
1120
+ if (!header) return [];
1121
+ return header
1122
+ .split(',')
1123
+ .map(part => {
1124
+ const [type, ...rest] = part.trim().split(';');
1125
+ const q = rest
1126
+ .find(p => p.trim().startsWith('q='))
1127
+ ?.split('=')[1];
1128
+ const quality = q ? parseFloat(q) : 1.0;
1129
+ return { type: type.trim().toLowerCase(), quality };
1130
+ })
1131
+ .filter(item => item.quality > 0)
1132
+ .sort((a, b) => b.quality - a.quality)
1133
+ .map(item => item.type);
1134
+ };
1135
+
1136
+ const accepts = (types) => {
1137
+ if (!Array.isArray(types)) types = [types];
1138
+ const accepted = parseAccept(req.headers['accept']);
1139
+
1140
+ for (const type of types) {
1141
+ const t = type.toLowerCase();
1142
+
1143
+ if (accepted.includes(t)) return type;
1144
+
1145
+ if (accepted.some(a => {
1146
+ if (a === '*/*') return true;
1147
+ if (a.endsWith('/*')) {
1148
+ const prefix = a.slice(0, -1);
1149
+ return t.startsWith(prefix);
1150
+ }
1151
+ return false;
1152
+ })) {
1153
+ return type;
1154
+ }
1155
+ }
1156
+
1157
+ return false;
1158
+ };
1159
+
1160
+ req.accepts = function (types) {
1161
+ return accepts(types);
1162
+ };
1163
+
1164
+ req.acceptsLanguages = function (langs) {
1165
+ if (!Array.isArray(langs)) langs = [langs];
1166
+ const accepted = parseAccept(req.headers['accept-language'] || '');
1167
+ for (const lang of langs) {
1168
+ const l = lang.toLowerCase();
1169
+ if (accepted.some(a => a === l || a.startsWith(l + '-'))) return lang;
1170
+ }
1171
+ return false;
1172
+ };
1173
+
1174
+ req.acceptsEncodings = function (encodings) {
1175
+ if (!Array.isArray(encodings)) encodings = [encodings];
1176
+ const accepted = parseAccept(req.headers['accept-encoding'] || '');
1177
+ for (const enc of encodings) {
1178
+ if (accepted.includes(enc.toLowerCase())) return enc;
1179
+ }
1180
+ return false;
1181
+ };
1182
+
1183
+ req.acceptsCharsets = function (charsets) {
1184
+ if (!Array.isArray(charsets)) charsets = [charsets];
1185
+ const accepted = parseAccept(req.headers['accept-charset'] || '');
1186
+ for (const charset of charsets) {
1187
+ if (accepted.includes(charset.toLowerCase())) return charset;
1188
+ }
1189
+ return false;
1190
+ };
1191
+
1192
+ req.is = function (type) {
1193
+ const ct = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
1194
+ if (!type) return ct;
1195
+ const t = type.toLowerCase();
1196
+ if (t.includes('/')) return ct === t;
1197
+ if (t === 'json') return ct.includes('json');
1198
+ if (t === 'urlencoded') return ct.includes('x-www-form-urlencoded');
1199
+ if (t === 'multipart') return ct.includes('multipart');
1200
+ return false;
1201
+ };
1429
1202
  }
1430
1203
 
1431
1204
  _enhanceResponse(req, res) {
@@ -1572,12 +1345,6 @@ ${cyan} (req, res, next) => {
1572
1345
  callback(null, html);
1573
1346
  resolve();
1574
1347
  } else {
1575
- /*
1576
- HBS cause error header already sent here ??
1577
- */
1578
- //res.statusCode = statusCode || 200;
1579
- //res.setHeader('Content-Type', 'text/html; charset=utf-8');
1580
- //responseSent = true;
1581
1348
  res.html(html);
1582
1349
  resolve();
1583
1350
  }
@@ -1841,7 +1608,7 @@ ${cyan} (req, res, next) => {
1841
1608
  logLines.push('---------------------------------------------');
1842
1609
  console.log('\n' + logLines.join('\n') + '\n');
1843
1610
  }
1844
-
1611
+
1845
1612
  listRoutes() {
1846
1613
  const routeEntries = [];
1847
1614
 
@@ -1916,7 +1683,6 @@ ${cyan} (req, res, next) => {
1916
1683
  listen() {
1917
1684
  const args = Array.from(arguments);
1918
1685
  const server = createServer(this._handleRequest.bind(this));
1919
-
1920
1686
  server.listen.apply(server, args);
1921
1687
  this.server = server;
1922
1688
  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.10",
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
  }