mikroserve 1.0.0 → 1.1.0
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/README.md +234 -1
- package/lib/MikroServe.d.mts +13 -0
- package/lib/MikroServe.d.ts +13 -0
- package/lib/MikroServe.js +360 -72
- package/lib/MikroServe.mjs +6 -5
- package/lib/RateLimiter.js +2 -1
- package/lib/RateLimiter.mjs +1 -1
- package/lib/Router.d.mts +22 -18
- package/lib/Router.d.ts +22 -18
- package/lib/Router.js +52 -36
- package/lib/Router.mjs +1 -1
- package/lib/{chunk-GUYBTPZH.mjs → chunk-7LU765PG.mjs} +53 -37
- package/lib/{chunk-TQN6BEGA.mjs → chunk-C4IW4XUH.mjs} +183 -40
- package/lib/{chunk-JJX5XRNB.mjs → chunk-DMNHVQTU.mjs} +6 -0
- package/lib/{chunk-ZFBBESGU.mjs → chunk-OF5DEOIU.mjs} +2 -1
- package/lib/chunk-VLQ7ZZIU.mjs +105 -0
- package/lib/{chunk-YOHL3T54.mjs → chunk-ZT2UGCN5.mjs} +29 -4
- package/lib/config.js +32 -3
- package/lib/config.mjs +2 -2
- package/lib/index.d.mts +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +360 -72
- package/lib/index.mjs +6 -5
- package/lib/interfaces/index.d.mts +10 -0
- package/lib/interfaces/index.d.ts +10 -0
- package/lib/utils/configDefaults.d.mts +2 -0
- package/lib/utils/configDefaults.d.ts +2 -0
- package/lib/utils/configDefaults.js +6 -0
- package/lib/utils/configDefaults.mjs +1 -1
- package/lib/utils/multipartParser.d.mts +19 -0
- package/lib/utils/multipartParser.d.ts +19 -0
- package/lib/utils/multipartParser.js +129 -0
- package/lib/utils/multipartParser.mjs +6 -0
- package/package.json +4 -4
package/lib/MikroServe.js
CHANGED
|
@@ -74,7 +74,8 @@ var RateLimiter = class {
|
|
|
74
74
|
const now = Date.now();
|
|
75
75
|
const key = ip || "unknown";
|
|
76
76
|
const entry = this.requests.get(key);
|
|
77
|
-
if (!entry || entry.resetTime < now)
|
|
77
|
+
if (!entry || entry.resetTime < now)
|
|
78
|
+
return Math.floor((now + this.windowMs) / 1e3);
|
|
78
79
|
return Math.floor(entry.resetTime / 1e3);
|
|
79
80
|
}
|
|
80
81
|
cleanup() {
|
|
@@ -92,64 +93,70 @@ var Router = class {
|
|
|
92
93
|
globalMiddlewares = [];
|
|
93
94
|
pathPatterns = /* @__PURE__ */ new Map();
|
|
94
95
|
/**
|
|
95
|
-
* Add a global middleware
|
|
96
|
+
* @description Add a global middleware.
|
|
96
97
|
*/
|
|
97
98
|
use(middleware) {
|
|
98
99
|
this.globalMiddlewares.push(middleware);
|
|
99
100
|
return this;
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
|
-
* Register a route
|
|
103
|
-
*/
|
|
104
|
-
register(method, path, handler, middlewares = []) {
|
|
105
|
-
this.routes.push({ method, path, handler, middlewares });
|
|
106
|
-
this.pathPatterns.set(path, this.createPathPattern(path));
|
|
107
|
-
return this;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Register a GET route
|
|
103
|
+
* @description Register a GET route.
|
|
111
104
|
*/
|
|
112
105
|
get(path, ...handlers) {
|
|
113
106
|
const handler = handlers.pop();
|
|
114
107
|
return this.register("GET", path, handler, handlers);
|
|
115
108
|
}
|
|
116
109
|
/**
|
|
117
|
-
* Register a POST route
|
|
110
|
+
* @description Register a POST route.
|
|
118
111
|
*/
|
|
119
112
|
post(path, ...handlers) {
|
|
120
113
|
const handler = handlers.pop();
|
|
121
114
|
return this.register("POST", path, handler, handlers);
|
|
122
115
|
}
|
|
123
116
|
/**
|
|
124
|
-
* Register a PUT route
|
|
117
|
+
* @description Register a PUT route.
|
|
125
118
|
*/
|
|
126
119
|
put(path, ...handlers) {
|
|
127
120
|
const handler = handlers.pop();
|
|
128
121
|
return this.register("PUT", path, handler, handlers);
|
|
129
122
|
}
|
|
130
123
|
/**
|
|
131
|
-
* Register a DELETE route
|
|
124
|
+
* @description Register a DELETE route.
|
|
132
125
|
*/
|
|
133
126
|
delete(path, ...handlers) {
|
|
134
127
|
const handler = handlers.pop();
|
|
135
128
|
return this.register("DELETE", path, handler, handlers);
|
|
136
129
|
}
|
|
137
130
|
/**
|
|
138
|
-
* Register a PATCH route
|
|
131
|
+
* @description Register a PATCH route.
|
|
139
132
|
*/
|
|
140
133
|
patch(path, ...handlers) {
|
|
141
134
|
const handler = handlers.pop();
|
|
142
135
|
return this.register("PATCH", path, handler, handlers);
|
|
143
136
|
}
|
|
144
137
|
/**
|
|
145
|
-
* Register
|
|
138
|
+
* @description Register a route for any HTTP method.
|
|
139
|
+
*/
|
|
140
|
+
any(path, ...handlers) {
|
|
141
|
+
const handler = handlers.pop();
|
|
142
|
+
const middlewares = handlers;
|
|
143
|
+
this.register("GET", path, handler, middlewares);
|
|
144
|
+
this.register("POST", path, handler, middlewares);
|
|
145
|
+
this.register("PUT", path, handler, middlewares);
|
|
146
|
+
this.register("DELETE", path, handler, middlewares);
|
|
147
|
+
this.register("PATCH", path, handler, middlewares);
|
|
148
|
+
this.register("OPTIONS", path, handler, middlewares);
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* @description Register an OPTIONS route.
|
|
146
153
|
*/
|
|
147
154
|
options(path, ...handlers) {
|
|
148
155
|
const handler = handlers.pop();
|
|
149
156
|
return this.register("OPTIONS", path, handler, handlers);
|
|
150
157
|
}
|
|
151
158
|
/**
|
|
152
|
-
* Match a request to a route
|
|
159
|
+
* @description Match a request to a route.
|
|
153
160
|
*/
|
|
154
161
|
match(method, path) {
|
|
155
162
|
for (const route of this.routes) {
|
|
@@ -167,22 +174,7 @@ var Router = class {
|
|
|
167
174
|
return null;
|
|
168
175
|
}
|
|
169
176
|
/**
|
|
170
|
-
*
|
|
171
|
-
*/
|
|
172
|
-
createPathPattern(path) {
|
|
173
|
-
const paramNames = [];
|
|
174
|
-
const pattern = path.replace(/\/:[^/]+/g, (match) => {
|
|
175
|
-
const paramName = match.slice(2);
|
|
176
|
-
paramNames.push(paramName);
|
|
177
|
-
return "/([^/]+)";
|
|
178
|
-
}).replace(/\/$/, "/?");
|
|
179
|
-
return {
|
|
180
|
-
pattern: new RegExp(`^${pattern}$`),
|
|
181
|
-
paramNames
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Handle a request and find the matching route
|
|
177
|
+
* @description Handle a request and find the matching route.
|
|
186
178
|
*/
|
|
187
179
|
async handle(req, res) {
|
|
188
180
|
const method = req.method || "GET";
|
|
@@ -200,12 +192,11 @@ var Router = class {
|
|
|
200
192
|
res,
|
|
201
193
|
params,
|
|
202
194
|
query,
|
|
203
|
-
// @ts-
|
|
195
|
+
// @ts-expect-error
|
|
204
196
|
body: req.body || {},
|
|
205
197
|
headers: req.headers,
|
|
206
198
|
path,
|
|
207
199
|
state: {},
|
|
208
|
-
// Add the missing state property
|
|
209
200
|
raw: () => res,
|
|
210
201
|
binary: (content, contentType = "application/octet-stream", status = 200) => ({
|
|
211
202
|
statusCode: status,
|
|
@@ -269,7 +260,6 @@ var Router = class {
|
|
|
269
260
|
headers: { "Content-Type": "text/html" }
|
|
270
261
|
}),
|
|
271
262
|
form: (content) => ({
|
|
272
|
-
// Make sure form method is included here
|
|
273
263
|
statusCode: code,
|
|
274
264
|
body: content,
|
|
275
265
|
headers: { "Content-Type": "application/x-www-form-urlencoded" }
|
|
@@ -287,7 +277,34 @@ var Router = class {
|
|
|
287
277
|
return this.executeMiddlewareChain(context, middlewares, route.handler);
|
|
288
278
|
}
|
|
289
279
|
/**
|
|
290
|
-
*
|
|
280
|
+
* @description Register a route with specified method.
|
|
281
|
+
*/
|
|
282
|
+
register(method, path, handler, middlewares = []) {
|
|
283
|
+
this.routes.push({ method, path, handler, middlewares });
|
|
284
|
+
this.pathPatterns.set(path, this.createPathPattern(path));
|
|
285
|
+
return this;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* @description Create a regex pattern for path matching.
|
|
289
|
+
*/
|
|
290
|
+
createPathPattern(path) {
|
|
291
|
+
const paramNames = [];
|
|
292
|
+
let pattern = path.replace(/\/:[^/]+/g, (match) => {
|
|
293
|
+
const paramName = match.slice(2);
|
|
294
|
+
paramNames.push(paramName);
|
|
295
|
+
return "/([^/]+)";
|
|
296
|
+
});
|
|
297
|
+
if (pattern.endsWith("/*")) {
|
|
298
|
+
pattern = `${pattern.slice(0, -2)}(?:/(.*))?`;
|
|
299
|
+
paramNames.push("wildcard");
|
|
300
|
+
} else pattern = pattern.replace(/\/$/, "/?");
|
|
301
|
+
return {
|
|
302
|
+
pattern: new RegExp(`^${pattern}$`),
|
|
303
|
+
paramNames
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* @description Execute middleware chain and final handler.
|
|
291
308
|
*/
|
|
292
309
|
async executeMiddlewareChain(context, middlewares, finalHandler) {
|
|
293
310
|
let currentIndex = 0;
|
|
@@ -316,6 +333,10 @@ var configDefaults = () => {
|
|
|
316
333
|
sslKey: "",
|
|
317
334
|
sslCa: "",
|
|
318
335
|
debug: getTruthyValue(process.env.DEBUG) || false,
|
|
336
|
+
maxBodySize: 1024 * 1024,
|
|
337
|
+
// 1MB
|
|
338
|
+
requestTimeout: 3e4,
|
|
339
|
+
// 30 seconds
|
|
319
340
|
rateLimit: {
|
|
320
341
|
enabled: true,
|
|
321
342
|
requestsPerMinute: 100
|
|
@@ -336,8 +357,18 @@ var baseConfig = (options) => ({
|
|
|
336
357
|
options: [
|
|
337
358
|
{ flag: "--port", path: "port", defaultValue: defaults.port },
|
|
338
359
|
{ flag: "--host", path: "host", defaultValue: defaults.host },
|
|
339
|
-
{
|
|
340
|
-
|
|
360
|
+
{
|
|
361
|
+
flag: "--https",
|
|
362
|
+
path: "useHttps",
|
|
363
|
+
defaultValue: defaults.useHttps,
|
|
364
|
+
isFlag: true
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
flag: "--http2",
|
|
368
|
+
path: "useHttp2",
|
|
369
|
+
defaultValue: defaults.useHttp2,
|
|
370
|
+
isFlag: true
|
|
371
|
+
},
|
|
341
372
|
{ flag: "--cert", path: "sslCert", defaultValue: defaults.sslCert },
|
|
342
373
|
{ flag: "--key", path: "sslKey", defaultValue: defaults.sslKey },
|
|
343
374
|
{ flag: "--ca", path: "sslCa", defaultValue: defaults.sslCa },
|
|
@@ -358,27 +389,148 @@ var baseConfig = (options) => ({
|
|
|
358
389
|
defaultValue: defaults.allowedDomains,
|
|
359
390
|
parser: import_mikroconf.parsers.array
|
|
360
391
|
},
|
|
361
|
-
{
|
|
392
|
+
{
|
|
393
|
+
flag: "--debug",
|
|
394
|
+
path: "debug",
|
|
395
|
+
defaultValue: defaults.debug,
|
|
396
|
+
isFlag: true
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
flag: "--max-body-size",
|
|
400
|
+
path: "maxBodySize",
|
|
401
|
+
defaultValue: defaults.maxBodySize
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
flag: "--request-timeout",
|
|
405
|
+
path: "requestTimeout",
|
|
406
|
+
defaultValue: defaults.requestTimeout
|
|
407
|
+
}
|
|
362
408
|
],
|
|
363
409
|
config: options
|
|
364
410
|
});
|
|
365
411
|
|
|
412
|
+
// src/utils/multipartParser.ts
|
|
413
|
+
function parseMultipartFormData(body, contentType) {
|
|
414
|
+
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
415
|
+
if (!boundaryMatch) {
|
|
416
|
+
throw new Error("Invalid multipart/form-data: missing boundary");
|
|
417
|
+
}
|
|
418
|
+
const boundary = boundaryMatch[1] || boundaryMatch[2];
|
|
419
|
+
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
420
|
+
const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
|
|
421
|
+
const fields = {};
|
|
422
|
+
const files = {};
|
|
423
|
+
const parts = splitByBoundary(body, boundaryBuffer);
|
|
424
|
+
for (const part of parts) {
|
|
425
|
+
if (part.length === 0) continue;
|
|
426
|
+
if (part.equals(endBoundaryBuffer.subarray(boundaryBuffer.length)))
|
|
427
|
+
continue;
|
|
428
|
+
const parsed = parsePart(part);
|
|
429
|
+
if (!parsed) continue;
|
|
430
|
+
const { name, filename, contentType: partContentType, data } = parsed;
|
|
431
|
+
if (filename) {
|
|
432
|
+
const file = {
|
|
433
|
+
filename,
|
|
434
|
+
contentType: partContentType || "application/octet-stream",
|
|
435
|
+
data,
|
|
436
|
+
size: data.length
|
|
437
|
+
};
|
|
438
|
+
if (files[name]) {
|
|
439
|
+
if (Array.isArray(files[name])) {
|
|
440
|
+
files[name].push(file);
|
|
441
|
+
} else {
|
|
442
|
+
files[name] = [files[name], file];
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
files[name] = file;
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
const value = data.toString("utf8");
|
|
449
|
+
if (fields[name]) {
|
|
450
|
+
if (Array.isArray(fields[name])) {
|
|
451
|
+
fields[name].push(value);
|
|
452
|
+
} else {
|
|
453
|
+
fields[name] = [fields[name], value];
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
fields[name] = value;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return { fields, files };
|
|
461
|
+
}
|
|
462
|
+
function splitByBoundary(buffer, boundary) {
|
|
463
|
+
const parts = [];
|
|
464
|
+
let start = 0;
|
|
465
|
+
while (start < buffer.length) {
|
|
466
|
+
const index = buffer.indexOf(boundary, start);
|
|
467
|
+
if (index === -1) break;
|
|
468
|
+
if (start !== index) {
|
|
469
|
+
parts.push(buffer.subarray(start, index));
|
|
470
|
+
}
|
|
471
|
+
start = index + boundary.length;
|
|
472
|
+
if (start < buffer.length && buffer[start] === 13 && buffer[start + 1] === 10) {
|
|
473
|
+
start += 2;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return parts;
|
|
477
|
+
}
|
|
478
|
+
function parsePart(part) {
|
|
479
|
+
const doubleCRLF = Buffer.from("\r\n\r\n");
|
|
480
|
+
const headerEndIndex = part.indexOf(doubleCRLF);
|
|
481
|
+
if (headerEndIndex === -1) return null;
|
|
482
|
+
const headersBuffer = part.subarray(0, headerEndIndex);
|
|
483
|
+
const dataBuffer = part.subarray(headerEndIndex + 4);
|
|
484
|
+
const headers = headersBuffer.toString("utf8");
|
|
485
|
+
const headerLines = headers.split("\r\n");
|
|
486
|
+
let disposition = "";
|
|
487
|
+
let name = "";
|
|
488
|
+
let filename;
|
|
489
|
+
let contentType;
|
|
490
|
+
for (const line of headerLines) {
|
|
491
|
+
const lowerLine = line.toLowerCase();
|
|
492
|
+
if (lowerLine.startsWith("content-disposition:")) {
|
|
493
|
+
disposition = line.substring("content-disposition:".length).trim();
|
|
494
|
+
const nameMatch = disposition.match(/name="([^"]+)"/);
|
|
495
|
+
if (nameMatch) {
|
|
496
|
+
name = nameMatch[1];
|
|
497
|
+
}
|
|
498
|
+
const filenameMatch = disposition.match(/filename="([^"]+)"/);
|
|
499
|
+
if (filenameMatch) {
|
|
500
|
+
filename = filenameMatch[1];
|
|
501
|
+
}
|
|
502
|
+
} else if (lowerLine.startsWith("content-type:")) {
|
|
503
|
+
contentType = line.substring("content-type:".length).trim();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!name) return null;
|
|
507
|
+
let data = dataBuffer;
|
|
508
|
+
if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
|
|
509
|
+
data = data.subarray(0, data.length - 2);
|
|
510
|
+
}
|
|
511
|
+
return { disposition, name, filename, contentType, data };
|
|
512
|
+
}
|
|
513
|
+
|
|
366
514
|
// src/MikroServe.ts
|
|
367
515
|
var MikroServe = class {
|
|
368
516
|
config;
|
|
369
517
|
rateLimiter;
|
|
370
518
|
router;
|
|
519
|
+
shutdownHandlers = [];
|
|
371
520
|
/**
|
|
372
521
|
* @description Creates a new MikroServe instance.
|
|
373
522
|
*/
|
|
374
523
|
constructor(options) {
|
|
375
|
-
const config = new import_mikroconf2.MikroConf(
|
|
524
|
+
const config = new import_mikroconf2.MikroConf(
|
|
525
|
+
baseConfig(options || {})
|
|
526
|
+
).get();
|
|
376
527
|
if (config.debug) console.log("Using configuration:", config);
|
|
377
528
|
this.config = config;
|
|
378
529
|
this.router = new Router();
|
|
379
530
|
const requestsPerMinute = config.rateLimit.requestsPerMinute || configDefaults().rateLimit.requestsPerMinute;
|
|
380
531
|
this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
|
|
381
|
-
if (config.rateLimit.enabled === true)
|
|
532
|
+
if (config.rateLimit.enabled === true)
|
|
533
|
+
this.use(this.rateLimitMiddleware.bind(this));
|
|
382
534
|
}
|
|
383
535
|
/**
|
|
384
536
|
* @description Register a global middleware.
|
|
@@ -422,6 +574,13 @@ var MikroServe = class {
|
|
|
422
574
|
this.router.patch(path, ...handlers);
|
|
423
575
|
return this;
|
|
424
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* @description Register a route that responds to any HTTP method.
|
|
579
|
+
*/
|
|
580
|
+
any(path, ...handlers) {
|
|
581
|
+
this.router.any(path, ...handlers);
|
|
582
|
+
return this;
|
|
583
|
+
}
|
|
425
584
|
/**
|
|
426
585
|
* @description Register an OPTIONS route.
|
|
427
586
|
*/
|
|
@@ -452,7 +611,9 @@ var MikroServe = class {
|
|
|
452
611
|
const boundRequestHandler = this.requestHandler.bind(this);
|
|
453
612
|
if (this.config.useHttp2) {
|
|
454
613
|
if (!this.config.sslCert || !this.config.sslKey)
|
|
455
|
-
throw new Error(
|
|
614
|
+
throw new Error(
|
|
615
|
+
"SSL certificate and key paths are required when useHttp2 is true"
|
|
616
|
+
);
|
|
456
617
|
try {
|
|
457
618
|
const httpsOptions = {
|
|
458
619
|
key: (0, import_node_fs.readFileSync)(this.config.sslKey),
|
|
@@ -462,12 +623,16 @@ var MikroServe = class {
|
|
|
462
623
|
return import_node_http2.default.createSecureServer(httpsOptions, boundRequestHandler);
|
|
463
624
|
} catch (error) {
|
|
464
625
|
if (error.message.includes("key values mismatch"))
|
|
465
|
-
throw new Error(
|
|
626
|
+
throw new Error(
|
|
627
|
+
`SSL certificate and key do not match: ${error.message}`
|
|
628
|
+
);
|
|
466
629
|
throw error;
|
|
467
630
|
}
|
|
468
631
|
} else if (this.config.useHttps) {
|
|
469
632
|
if (!this.config.sslCert || !this.config.sslKey)
|
|
470
|
-
throw new Error(
|
|
633
|
+
throw new Error(
|
|
634
|
+
"SSL certificate and key paths are required when useHttps is true"
|
|
635
|
+
);
|
|
471
636
|
try {
|
|
472
637
|
const httpsOptions = {
|
|
473
638
|
key: (0, import_node_fs.readFileSync)(this.config.sslKey),
|
|
@@ -477,7 +642,9 @@ var MikroServe = class {
|
|
|
477
642
|
return import_node_https.default.createServer(httpsOptions, boundRequestHandler);
|
|
478
643
|
} catch (error) {
|
|
479
644
|
if (error.message.includes("key values mismatch"))
|
|
480
|
-
throw new Error(
|
|
645
|
+
throw new Error(
|
|
646
|
+
`SSL certificate and key do not match: ${error.message}`
|
|
647
|
+
);
|
|
481
648
|
throw error;
|
|
482
649
|
}
|
|
483
650
|
}
|
|
@@ -488,12 +655,18 @@ var MikroServe = class {
|
|
|
488
655
|
*/
|
|
489
656
|
async rateLimitMiddleware(context, next) {
|
|
490
657
|
const ip = context.req.socket.remoteAddress || "unknown";
|
|
491
|
-
context.res.setHeader(
|
|
658
|
+
context.res.setHeader(
|
|
659
|
+
"X-RateLimit-Limit",
|
|
660
|
+
this.rateLimiter.getLimit().toString()
|
|
661
|
+
);
|
|
492
662
|
context.res.setHeader(
|
|
493
663
|
"X-RateLimit-Remaining",
|
|
494
664
|
this.rateLimiter.getRemainingRequests(ip).toString()
|
|
495
665
|
);
|
|
496
|
-
context.res.setHeader(
|
|
666
|
+
context.res.setHeader(
|
|
667
|
+
"X-RateLimit-Reset",
|
|
668
|
+
this.rateLimiter.getResetTime(ip).toString()
|
|
669
|
+
);
|
|
497
670
|
if (!this.rateLimiter.isAllowed(ip)) {
|
|
498
671
|
return {
|
|
499
672
|
statusCode: 429,
|
|
@@ -580,44 +753,87 @@ var MikroServe = class {
|
|
|
580
753
|
return new Promise((resolve, reject) => {
|
|
581
754
|
const bodyChunks = [];
|
|
582
755
|
let bodySize = 0;
|
|
583
|
-
const
|
|
584
|
-
let
|
|
756
|
+
const maxBodySize = this.config.maxBodySize;
|
|
757
|
+
let settled = false;
|
|
758
|
+
let timeoutId = null;
|
|
585
759
|
const isDebug = this.config.debug;
|
|
586
760
|
const contentType = req.headers["content-type"] || "";
|
|
587
761
|
if (isDebug) {
|
|
588
762
|
console.log("Content-Type:", contentType);
|
|
589
763
|
}
|
|
764
|
+
if (this.config.requestTimeout > 0) {
|
|
765
|
+
timeoutId = setTimeout(() => {
|
|
766
|
+
if (!settled) {
|
|
767
|
+
settled = true;
|
|
768
|
+
if (isDebug) console.log("Request timeout exceeded");
|
|
769
|
+
reject(new Error("Request timeout"));
|
|
770
|
+
}
|
|
771
|
+
}, this.config.requestTimeout);
|
|
772
|
+
}
|
|
773
|
+
const cleanup = () => {
|
|
774
|
+
if (timeoutId) {
|
|
775
|
+
clearTimeout(timeoutId);
|
|
776
|
+
timeoutId = null;
|
|
777
|
+
}
|
|
778
|
+
};
|
|
590
779
|
req.on("data", (chunk) => {
|
|
780
|
+
if (settled) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
591
783
|
bodySize += chunk.length;
|
|
592
|
-
if (isDebug)
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
784
|
+
if (isDebug)
|
|
785
|
+
console.log(
|
|
786
|
+
`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`
|
|
787
|
+
);
|
|
788
|
+
if (bodySize > maxBodySize) {
|
|
789
|
+
settled = true;
|
|
790
|
+
cleanup();
|
|
791
|
+
if (isDebug)
|
|
792
|
+
console.log(
|
|
793
|
+
`Body size exceeded limit: ${bodySize} > ${maxBodySize}`
|
|
794
|
+
);
|
|
596
795
|
reject(new Error("Request body too large"));
|
|
597
796
|
return;
|
|
598
797
|
}
|
|
599
|
-
|
|
798
|
+
bodyChunks.push(chunk);
|
|
600
799
|
});
|
|
601
800
|
req.on("end", () => {
|
|
602
|
-
if (
|
|
801
|
+
if (settled) return;
|
|
802
|
+
settled = true;
|
|
803
|
+
cleanup();
|
|
603
804
|
if (isDebug) console.log(`Request body complete: ${bodySize} bytes`);
|
|
604
805
|
try {
|
|
605
806
|
if (bodyChunks.length > 0) {
|
|
606
|
-
const
|
|
807
|
+
const bodyBuffer = Buffer.concat(bodyChunks);
|
|
607
808
|
if (contentType.includes("application/json")) {
|
|
608
809
|
try {
|
|
810
|
+
const bodyString = bodyBuffer.toString("utf8");
|
|
609
811
|
resolve(JSON.parse(bodyString));
|
|
610
812
|
} catch (error) {
|
|
611
|
-
reject(
|
|
813
|
+
reject(
|
|
814
|
+
new Error(`Invalid JSON in request body: ${error.message}`)
|
|
815
|
+
);
|
|
612
816
|
}
|
|
613
817
|
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
818
|
+
const bodyString = bodyBuffer.toString("utf8");
|
|
614
819
|
const formData = {};
|
|
615
820
|
new URLSearchParams(bodyString).forEach((value, key) => {
|
|
616
821
|
formData[key] = value;
|
|
617
822
|
});
|
|
618
823
|
resolve(formData);
|
|
824
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
825
|
+
try {
|
|
826
|
+
const parsed = parseMultipartFormData(bodyBuffer, contentType);
|
|
827
|
+
resolve(parsed);
|
|
828
|
+
} catch (error) {
|
|
829
|
+
reject(
|
|
830
|
+
new Error(`Invalid multipart form data: ${error.message}`)
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
} else if (this.isBinaryContentType(contentType)) {
|
|
834
|
+
resolve(bodyBuffer);
|
|
619
835
|
} else {
|
|
620
|
-
resolve(
|
|
836
|
+
resolve(bodyBuffer.toString("utf8"));
|
|
621
837
|
}
|
|
622
838
|
} else {
|
|
623
839
|
resolve({});
|
|
@@ -627,24 +843,61 @@ var MikroServe = class {
|
|
|
627
843
|
}
|
|
628
844
|
});
|
|
629
845
|
req.on("error", (error) => {
|
|
630
|
-
if (!
|
|
846
|
+
if (!settled) {
|
|
847
|
+
settled = true;
|
|
848
|
+
cleanup();
|
|
849
|
+
reject(new Error(`Error reading request body: ${error.message}`));
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
req.on("close", () => {
|
|
853
|
+
cleanup();
|
|
631
854
|
});
|
|
632
855
|
});
|
|
633
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* @description Checks if a content type is binary.
|
|
859
|
+
*/
|
|
860
|
+
isBinaryContentType(contentType) {
|
|
861
|
+
const binaryTypes = [
|
|
862
|
+
"application/octet-stream",
|
|
863
|
+
"application/pdf",
|
|
864
|
+
"application/zip",
|
|
865
|
+
"application/gzip",
|
|
866
|
+
"application/x-tar",
|
|
867
|
+
"application/x-rar-compressed",
|
|
868
|
+
"application/x-7z-compressed",
|
|
869
|
+
"image/",
|
|
870
|
+
"video/",
|
|
871
|
+
"audio/",
|
|
872
|
+
"application/vnd.ms-excel",
|
|
873
|
+
"application/vnd.openxmlformats-officedocument",
|
|
874
|
+
"application/msword",
|
|
875
|
+
"application/vnd.ms-powerpoint"
|
|
876
|
+
];
|
|
877
|
+
return binaryTypes.some((type) => contentType.includes(type));
|
|
878
|
+
}
|
|
634
879
|
/**
|
|
635
880
|
* @description CORS middleware.
|
|
636
881
|
*/
|
|
637
882
|
setCorsHeaders(res, req) {
|
|
638
883
|
const origin = req.headers.origin;
|
|
639
884
|
const { allowedDomains = ["*"] } = this.config;
|
|
640
|
-
if (!origin || allowedDomains.length === 0)
|
|
641
|
-
|
|
885
|
+
if (!origin || allowedDomains.length === 0)
|
|
886
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
887
|
+
else if (allowedDomains.includes("*"))
|
|
888
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
642
889
|
else if (allowedDomains.includes(origin)) {
|
|
643
890
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
644
891
|
res.setHeader("Vary", "Origin");
|
|
645
892
|
}
|
|
646
|
-
res.setHeader(
|
|
647
|
-
|
|
893
|
+
res.setHeader(
|
|
894
|
+
"Access-Control-Allow-Methods",
|
|
895
|
+
"GET, POST, PUT, DELETE, PATCH, OPTIONS"
|
|
896
|
+
);
|
|
897
|
+
res.setHeader(
|
|
898
|
+
"Access-Control-Allow-Headers",
|
|
899
|
+
"Content-Type, Authorization"
|
|
900
|
+
);
|
|
648
901
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
649
902
|
}
|
|
650
903
|
/**
|
|
@@ -687,11 +940,15 @@ var MikroServe = class {
|
|
|
687
940
|
else if (typeof response.body === "string") res.end(response.body);
|
|
688
941
|
else res.end(JSON.stringify(response.body));
|
|
689
942
|
} else {
|
|
690
|
-
console.warn(
|
|
943
|
+
console.warn(
|
|
944
|
+
"Unexpected response object type without writeHead/end methods"
|
|
945
|
+
);
|
|
691
946
|
res.writeHead?.(response.statusCode, headers);
|
|
692
|
-
if (response.body === null || response.body === void 0)
|
|
947
|
+
if (response.body === null || response.body === void 0)
|
|
948
|
+
res.end?.();
|
|
693
949
|
else if (response.isRaw) res.end?.(response.body);
|
|
694
|
-
else if (typeof response.body === "string")
|
|
950
|
+
else if (typeof response.body === "string")
|
|
951
|
+
res.end?.(response.body);
|
|
695
952
|
else res.end?.(JSON.stringify(response.body));
|
|
696
953
|
}
|
|
697
954
|
}
|
|
@@ -702,15 +959,46 @@ var MikroServe = class {
|
|
|
702
959
|
const shutdown = (error) => {
|
|
703
960
|
console.log("Shutting down MikroServe server...");
|
|
704
961
|
if (error) console.error("Error:", error);
|
|
962
|
+
this.cleanupShutdownHandlers();
|
|
705
963
|
server.close(() => {
|
|
706
964
|
console.log("Server closed successfully");
|
|
707
|
-
|
|
965
|
+
if (process.env.NODE_ENV !== "test" && process.env.VITEST !== "true") {
|
|
966
|
+
setImmediate(() => process.exit(error ? 1 : 0));
|
|
967
|
+
}
|
|
708
968
|
});
|
|
709
969
|
};
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
970
|
+
const sigintHandler = () => shutdown();
|
|
971
|
+
const sigtermHandler = () => shutdown();
|
|
972
|
+
const uncaughtExceptionHandler = (error) => shutdown(error);
|
|
973
|
+
const unhandledRejectionHandler = (error) => shutdown(error);
|
|
974
|
+
this.shutdownHandlers = [
|
|
975
|
+
sigintHandler,
|
|
976
|
+
sigtermHandler,
|
|
977
|
+
uncaughtExceptionHandler,
|
|
978
|
+
unhandledRejectionHandler
|
|
979
|
+
];
|
|
980
|
+
process.on("SIGINT", sigintHandler);
|
|
981
|
+
process.on("SIGTERM", sigtermHandler);
|
|
982
|
+
process.on("uncaughtException", uncaughtExceptionHandler);
|
|
983
|
+
process.on("unhandledRejection", unhandledRejectionHandler);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* @description Cleans up shutdown event listeners to prevent memory leaks.
|
|
987
|
+
*/
|
|
988
|
+
cleanupShutdownHandlers() {
|
|
989
|
+
if (this.shutdownHandlers.length > 0) {
|
|
990
|
+
const [
|
|
991
|
+
sigintHandler,
|
|
992
|
+
sigtermHandler,
|
|
993
|
+
uncaughtExceptionHandler,
|
|
994
|
+
unhandledRejectionHandler
|
|
995
|
+
] = this.shutdownHandlers;
|
|
996
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
997
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
998
|
+
process.removeListener("uncaughtException", uncaughtExceptionHandler);
|
|
999
|
+
process.removeListener("unhandledRejection", unhandledRejectionHandler);
|
|
1000
|
+
this.shutdownHandlers = [];
|
|
1001
|
+
}
|
|
714
1002
|
}
|
|
715
1003
|
};
|
|
716
1004
|
// Annotate the CommonJS export names for ESM import in node:
|
package/lib/MikroServe.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MikroServe
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
7
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-C4IW4XUH.mjs";
|
|
4
|
+
import "./chunk-OF5DEOIU.mjs";
|
|
5
|
+
import "./chunk-7LU765PG.mjs";
|
|
6
|
+
import "./chunk-ZT2UGCN5.mjs";
|
|
7
|
+
import "./chunk-DMNHVQTU.mjs";
|
|
8
|
+
import "./chunk-VLQ7ZZIU.mjs";
|
|
8
9
|
export {
|
|
9
10
|
MikroServe
|
|
10
11
|
};
|
package/lib/RateLimiter.js
CHANGED
|
@@ -57,7 +57,8 @@ var RateLimiter = class {
|
|
|
57
57
|
const now = Date.now();
|
|
58
58
|
const key = ip || "unknown";
|
|
59
59
|
const entry = this.requests.get(key);
|
|
60
|
-
if (!entry || entry.resetTime < now)
|
|
60
|
+
if (!entry || entry.resetTime < now)
|
|
61
|
+
return Math.floor((now + this.windowMs) / 1e3);
|
|
61
62
|
return Math.floor(entry.resetTime / 1e3);
|
|
62
63
|
}
|
|
63
64
|
cleanup() {
|