mikroserve 1.0.1 → 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 +233 -0
- package/lib/MikroServe.d.mts +9 -0
- package/lib/MikroServe.d.ts +9 -0
- package/lib/MikroServe.js +302 -37
- package/lib/MikroServe.mjs +6 -5
- package/lib/RateLimiter.js +2 -1
- package/lib/RateLimiter.mjs +1 -1
- package/lib/Router.js +1 -1
- package/lib/Router.mjs +1 -1
- package/lib/{chunk-YKRH6T5M.mjs → chunk-7LU765PG.mjs} +2 -2
- package/lib/{chunk-N5ZQZGGT.mjs → chunk-C4IW4XUH.mjs} +176 -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 +302 -37
- 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/index.js
CHANGED
|
@@ -76,7 +76,8 @@ var RateLimiter = class {
|
|
|
76
76
|
const now = Date.now();
|
|
77
77
|
const key = ip || "unknown";
|
|
78
78
|
const entry = this.requests.get(key);
|
|
79
|
-
if (!entry || entry.resetTime < now)
|
|
79
|
+
if (!entry || entry.resetTime < now)
|
|
80
|
+
return Math.floor((now + this.windowMs) / 1e3);
|
|
80
81
|
return Math.floor(entry.resetTime / 1e3);
|
|
81
82
|
}
|
|
82
83
|
cleanup() {
|
|
@@ -193,7 +194,7 @@ var Router = class {
|
|
|
193
194
|
res,
|
|
194
195
|
params,
|
|
195
196
|
query,
|
|
196
|
-
// @ts-
|
|
197
|
+
// @ts-expect-error
|
|
197
198
|
body: req.body || {},
|
|
198
199
|
headers: req.headers,
|
|
199
200
|
path,
|
|
@@ -334,6 +335,10 @@ var configDefaults = () => {
|
|
|
334
335
|
sslKey: "",
|
|
335
336
|
sslCa: "",
|
|
336
337
|
debug: getTruthyValue(process.env.DEBUG) || false,
|
|
338
|
+
maxBodySize: 1024 * 1024,
|
|
339
|
+
// 1MB
|
|
340
|
+
requestTimeout: 3e4,
|
|
341
|
+
// 30 seconds
|
|
337
342
|
rateLimit: {
|
|
338
343
|
enabled: true,
|
|
339
344
|
requestsPerMinute: 100
|
|
@@ -354,8 +359,18 @@ var baseConfig = (options) => ({
|
|
|
354
359
|
options: [
|
|
355
360
|
{ flag: "--port", path: "port", defaultValue: defaults.port },
|
|
356
361
|
{ flag: "--host", path: "host", defaultValue: defaults.host },
|
|
357
|
-
{
|
|
358
|
-
|
|
362
|
+
{
|
|
363
|
+
flag: "--https",
|
|
364
|
+
path: "useHttps",
|
|
365
|
+
defaultValue: defaults.useHttps,
|
|
366
|
+
isFlag: true
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
flag: "--http2",
|
|
370
|
+
path: "useHttp2",
|
|
371
|
+
defaultValue: defaults.useHttp2,
|
|
372
|
+
isFlag: true
|
|
373
|
+
},
|
|
359
374
|
{ flag: "--cert", path: "sslCert", defaultValue: defaults.sslCert },
|
|
360
375
|
{ flag: "--key", path: "sslKey", defaultValue: defaults.sslKey },
|
|
361
376
|
{ flag: "--ca", path: "sslCa", defaultValue: defaults.sslCa },
|
|
@@ -376,27 +391,148 @@ var baseConfig = (options) => ({
|
|
|
376
391
|
defaultValue: defaults.allowedDomains,
|
|
377
392
|
parser: import_mikroconf.parsers.array
|
|
378
393
|
},
|
|
379
|
-
{
|
|
394
|
+
{
|
|
395
|
+
flag: "--debug",
|
|
396
|
+
path: "debug",
|
|
397
|
+
defaultValue: defaults.debug,
|
|
398
|
+
isFlag: true
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
flag: "--max-body-size",
|
|
402
|
+
path: "maxBodySize",
|
|
403
|
+
defaultValue: defaults.maxBodySize
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
flag: "--request-timeout",
|
|
407
|
+
path: "requestTimeout",
|
|
408
|
+
defaultValue: defaults.requestTimeout
|
|
409
|
+
}
|
|
380
410
|
],
|
|
381
411
|
config: options
|
|
382
412
|
});
|
|
383
413
|
|
|
414
|
+
// src/utils/multipartParser.ts
|
|
415
|
+
function parseMultipartFormData(body, contentType) {
|
|
416
|
+
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
417
|
+
if (!boundaryMatch) {
|
|
418
|
+
throw new Error("Invalid multipart/form-data: missing boundary");
|
|
419
|
+
}
|
|
420
|
+
const boundary = boundaryMatch[1] || boundaryMatch[2];
|
|
421
|
+
const boundaryBuffer = Buffer.from(`--${boundary}`);
|
|
422
|
+
const endBoundaryBuffer = Buffer.from(`--${boundary}--`);
|
|
423
|
+
const fields = {};
|
|
424
|
+
const files = {};
|
|
425
|
+
const parts = splitByBoundary(body, boundaryBuffer);
|
|
426
|
+
for (const part of parts) {
|
|
427
|
+
if (part.length === 0) continue;
|
|
428
|
+
if (part.equals(endBoundaryBuffer.subarray(boundaryBuffer.length)))
|
|
429
|
+
continue;
|
|
430
|
+
const parsed = parsePart(part);
|
|
431
|
+
if (!parsed) continue;
|
|
432
|
+
const { name, filename, contentType: partContentType, data } = parsed;
|
|
433
|
+
if (filename) {
|
|
434
|
+
const file = {
|
|
435
|
+
filename,
|
|
436
|
+
contentType: partContentType || "application/octet-stream",
|
|
437
|
+
data,
|
|
438
|
+
size: data.length
|
|
439
|
+
};
|
|
440
|
+
if (files[name]) {
|
|
441
|
+
if (Array.isArray(files[name])) {
|
|
442
|
+
files[name].push(file);
|
|
443
|
+
} else {
|
|
444
|
+
files[name] = [files[name], file];
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
files[name] = file;
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
const value = data.toString("utf8");
|
|
451
|
+
if (fields[name]) {
|
|
452
|
+
if (Array.isArray(fields[name])) {
|
|
453
|
+
fields[name].push(value);
|
|
454
|
+
} else {
|
|
455
|
+
fields[name] = [fields[name], value];
|
|
456
|
+
}
|
|
457
|
+
} else {
|
|
458
|
+
fields[name] = value;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { fields, files };
|
|
463
|
+
}
|
|
464
|
+
function splitByBoundary(buffer, boundary) {
|
|
465
|
+
const parts = [];
|
|
466
|
+
let start = 0;
|
|
467
|
+
while (start < buffer.length) {
|
|
468
|
+
const index = buffer.indexOf(boundary, start);
|
|
469
|
+
if (index === -1) break;
|
|
470
|
+
if (start !== index) {
|
|
471
|
+
parts.push(buffer.subarray(start, index));
|
|
472
|
+
}
|
|
473
|
+
start = index + boundary.length;
|
|
474
|
+
if (start < buffer.length && buffer[start] === 13 && buffer[start + 1] === 10) {
|
|
475
|
+
start += 2;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return parts;
|
|
479
|
+
}
|
|
480
|
+
function parsePart(part) {
|
|
481
|
+
const doubleCRLF = Buffer.from("\r\n\r\n");
|
|
482
|
+
const headerEndIndex = part.indexOf(doubleCRLF);
|
|
483
|
+
if (headerEndIndex === -1) return null;
|
|
484
|
+
const headersBuffer = part.subarray(0, headerEndIndex);
|
|
485
|
+
const dataBuffer = part.subarray(headerEndIndex + 4);
|
|
486
|
+
const headers = headersBuffer.toString("utf8");
|
|
487
|
+
const headerLines = headers.split("\r\n");
|
|
488
|
+
let disposition = "";
|
|
489
|
+
let name = "";
|
|
490
|
+
let filename;
|
|
491
|
+
let contentType;
|
|
492
|
+
for (const line of headerLines) {
|
|
493
|
+
const lowerLine = line.toLowerCase();
|
|
494
|
+
if (lowerLine.startsWith("content-disposition:")) {
|
|
495
|
+
disposition = line.substring("content-disposition:".length).trim();
|
|
496
|
+
const nameMatch = disposition.match(/name="([^"]+)"/);
|
|
497
|
+
if (nameMatch) {
|
|
498
|
+
name = nameMatch[1];
|
|
499
|
+
}
|
|
500
|
+
const filenameMatch = disposition.match(/filename="([^"]+)"/);
|
|
501
|
+
if (filenameMatch) {
|
|
502
|
+
filename = filenameMatch[1];
|
|
503
|
+
}
|
|
504
|
+
} else if (lowerLine.startsWith("content-type:")) {
|
|
505
|
+
contentType = line.substring("content-type:".length).trim();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!name) return null;
|
|
509
|
+
let data = dataBuffer;
|
|
510
|
+
if (data.length >= 2 && data[data.length - 2] === 13 && data[data.length - 1] === 10) {
|
|
511
|
+
data = data.subarray(0, data.length - 2);
|
|
512
|
+
}
|
|
513
|
+
return { disposition, name, filename, contentType, data };
|
|
514
|
+
}
|
|
515
|
+
|
|
384
516
|
// src/MikroServe.ts
|
|
385
517
|
var MikroServe = class {
|
|
386
518
|
config;
|
|
387
519
|
rateLimiter;
|
|
388
520
|
router;
|
|
521
|
+
shutdownHandlers = [];
|
|
389
522
|
/**
|
|
390
523
|
* @description Creates a new MikroServe instance.
|
|
391
524
|
*/
|
|
392
525
|
constructor(options) {
|
|
393
|
-
const config = new import_mikroconf2.MikroConf(
|
|
526
|
+
const config = new import_mikroconf2.MikroConf(
|
|
527
|
+
baseConfig(options || {})
|
|
528
|
+
).get();
|
|
394
529
|
if (config.debug) console.log("Using configuration:", config);
|
|
395
530
|
this.config = config;
|
|
396
531
|
this.router = new Router();
|
|
397
532
|
const requestsPerMinute = config.rateLimit.requestsPerMinute || configDefaults().rateLimit.requestsPerMinute;
|
|
398
533
|
this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
|
|
399
|
-
if (config.rateLimit.enabled === true)
|
|
534
|
+
if (config.rateLimit.enabled === true)
|
|
535
|
+
this.use(this.rateLimitMiddleware.bind(this));
|
|
400
536
|
}
|
|
401
537
|
/**
|
|
402
538
|
* @description Register a global middleware.
|
|
@@ -477,7 +613,9 @@ var MikroServe = class {
|
|
|
477
613
|
const boundRequestHandler = this.requestHandler.bind(this);
|
|
478
614
|
if (this.config.useHttp2) {
|
|
479
615
|
if (!this.config.sslCert || !this.config.sslKey)
|
|
480
|
-
throw new Error(
|
|
616
|
+
throw new Error(
|
|
617
|
+
"SSL certificate and key paths are required when useHttp2 is true"
|
|
618
|
+
);
|
|
481
619
|
try {
|
|
482
620
|
const httpsOptions = {
|
|
483
621
|
key: (0, import_node_fs.readFileSync)(this.config.sslKey),
|
|
@@ -487,12 +625,16 @@ var MikroServe = class {
|
|
|
487
625
|
return import_node_http2.default.createSecureServer(httpsOptions, boundRequestHandler);
|
|
488
626
|
} catch (error) {
|
|
489
627
|
if (error.message.includes("key values mismatch"))
|
|
490
|
-
throw new Error(
|
|
628
|
+
throw new Error(
|
|
629
|
+
`SSL certificate and key do not match: ${error.message}`
|
|
630
|
+
);
|
|
491
631
|
throw error;
|
|
492
632
|
}
|
|
493
633
|
} else if (this.config.useHttps) {
|
|
494
634
|
if (!this.config.sslCert || !this.config.sslKey)
|
|
495
|
-
throw new Error(
|
|
635
|
+
throw new Error(
|
|
636
|
+
"SSL certificate and key paths are required when useHttps is true"
|
|
637
|
+
);
|
|
496
638
|
try {
|
|
497
639
|
const httpsOptions = {
|
|
498
640
|
key: (0, import_node_fs.readFileSync)(this.config.sslKey),
|
|
@@ -502,7 +644,9 @@ var MikroServe = class {
|
|
|
502
644
|
return import_node_https.default.createServer(httpsOptions, boundRequestHandler);
|
|
503
645
|
} catch (error) {
|
|
504
646
|
if (error.message.includes("key values mismatch"))
|
|
505
|
-
throw new Error(
|
|
647
|
+
throw new Error(
|
|
648
|
+
`SSL certificate and key do not match: ${error.message}`
|
|
649
|
+
);
|
|
506
650
|
throw error;
|
|
507
651
|
}
|
|
508
652
|
}
|
|
@@ -513,12 +657,18 @@ var MikroServe = class {
|
|
|
513
657
|
*/
|
|
514
658
|
async rateLimitMiddleware(context, next) {
|
|
515
659
|
const ip = context.req.socket.remoteAddress || "unknown";
|
|
516
|
-
context.res.setHeader(
|
|
660
|
+
context.res.setHeader(
|
|
661
|
+
"X-RateLimit-Limit",
|
|
662
|
+
this.rateLimiter.getLimit().toString()
|
|
663
|
+
);
|
|
517
664
|
context.res.setHeader(
|
|
518
665
|
"X-RateLimit-Remaining",
|
|
519
666
|
this.rateLimiter.getRemainingRequests(ip).toString()
|
|
520
667
|
);
|
|
521
|
-
context.res.setHeader(
|
|
668
|
+
context.res.setHeader(
|
|
669
|
+
"X-RateLimit-Reset",
|
|
670
|
+
this.rateLimiter.getResetTime(ip).toString()
|
|
671
|
+
);
|
|
522
672
|
if (!this.rateLimiter.isAllowed(ip)) {
|
|
523
673
|
return {
|
|
524
674
|
statusCode: 429,
|
|
@@ -605,44 +755,87 @@ var MikroServe = class {
|
|
|
605
755
|
return new Promise((resolve, reject) => {
|
|
606
756
|
const bodyChunks = [];
|
|
607
757
|
let bodySize = 0;
|
|
608
|
-
const
|
|
609
|
-
let
|
|
758
|
+
const maxBodySize = this.config.maxBodySize;
|
|
759
|
+
let settled = false;
|
|
760
|
+
let timeoutId = null;
|
|
610
761
|
const isDebug = this.config.debug;
|
|
611
762
|
const contentType = req.headers["content-type"] || "";
|
|
612
763
|
if (isDebug) {
|
|
613
764
|
console.log("Content-Type:", contentType);
|
|
614
765
|
}
|
|
766
|
+
if (this.config.requestTimeout > 0) {
|
|
767
|
+
timeoutId = setTimeout(() => {
|
|
768
|
+
if (!settled) {
|
|
769
|
+
settled = true;
|
|
770
|
+
if (isDebug) console.log("Request timeout exceeded");
|
|
771
|
+
reject(new Error("Request timeout"));
|
|
772
|
+
}
|
|
773
|
+
}, this.config.requestTimeout);
|
|
774
|
+
}
|
|
775
|
+
const cleanup = () => {
|
|
776
|
+
if (timeoutId) {
|
|
777
|
+
clearTimeout(timeoutId);
|
|
778
|
+
timeoutId = null;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
615
781
|
req.on("data", (chunk) => {
|
|
782
|
+
if (settled) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
616
785
|
bodySize += chunk.length;
|
|
617
|
-
if (isDebug)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
786
|
+
if (isDebug)
|
|
787
|
+
console.log(
|
|
788
|
+
`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`
|
|
789
|
+
);
|
|
790
|
+
if (bodySize > maxBodySize) {
|
|
791
|
+
settled = true;
|
|
792
|
+
cleanup();
|
|
793
|
+
if (isDebug)
|
|
794
|
+
console.log(
|
|
795
|
+
`Body size exceeded limit: ${bodySize} > ${maxBodySize}`
|
|
796
|
+
);
|
|
621
797
|
reject(new Error("Request body too large"));
|
|
622
798
|
return;
|
|
623
799
|
}
|
|
624
|
-
|
|
800
|
+
bodyChunks.push(chunk);
|
|
625
801
|
});
|
|
626
802
|
req.on("end", () => {
|
|
627
|
-
if (
|
|
803
|
+
if (settled) return;
|
|
804
|
+
settled = true;
|
|
805
|
+
cleanup();
|
|
628
806
|
if (isDebug) console.log(`Request body complete: ${bodySize} bytes`);
|
|
629
807
|
try {
|
|
630
808
|
if (bodyChunks.length > 0) {
|
|
631
|
-
const
|
|
809
|
+
const bodyBuffer = Buffer.concat(bodyChunks);
|
|
632
810
|
if (contentType.includes("application/json")) {
|
|
633
811
|
try {
|
|
812
|
+
const bodyString = bodyBuffer.toString("utf8");
|
|
634
813
|
resolve(JSON.parse(bodyString));
|
|
635
814
|
} catch (error) {
|
|
636
|
-
reject(
|
|
815
|
+
reject(
|
|
816
|
+
new Error(`Invalid JSON in request body: ${error.message}`)
|
|
817
|
+
);
|
|
637
818
|
}
|
|
638
819
|
} else if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
820
|
+
const bodyString = bodyBuffer.toString("utf8");
|
|
639
821
|
const formData = {};
|
|
640
822
|
new URLSearchParams(bodyString).forEach((value, key) => {
|
|
641
823
|
formData[key] = value;
|
|
642
824
|
});
|
|
643
825
|
resolve(formData);
|
|
826
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
827
|
+
try {
|
|
828
|
+
const parsed = parseMultipartFormData(bodyBuffer, contentType);
|
|
829
|
+
resolve(parsed);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
reject(
|
|
832
|
+
new Error(`Invalid multipart form data: ${error.message}`)
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
} else if (this.isBinaryContentType(contentType)) {
|
|
836
|
+
resolve(bodyBuffer);
|
|
644
837
|
} else {
|
|
645
|
-
resolve(
|
|
838
|
+
resolve(bodyBuffer.toString("utf8"));
|
|
646
839
|
}
|
|
647
840
|
} else {
|
|
648
841
|
resolve({});
|
|
@@ -652,24 +845,61 @@ var MikroServe = class {
|
|
|
652
845
|
}
|
|
653
846
|
});
|
|
654
847
|
req.on("error", (error) => {
|
|
655
|
-
if (!
|
|
848
|
+
if (!settled) {
|
|
849
|
+
settled = true;
|
|
850
|
+
cleanup();
|
|
851
|
+
reject(new Error(`Error reading request body: ${error.message}`));
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
req.on("close", () => {
|
|
855
|
+
cleanup();
|
|
656
856
|
});
|
|
657
857
|
});
|
|
658
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* @description Checks if a content type is binary.
|
|
861
|
+
*/
|
|
862
|
+
isBinaryContentType(contentType) {
|
|
863
|
+
const binaryTypes = [
|
|
864
|
+
"application/octet-stream",
|
|
865
|
+
"application/pdf",
|
|
866
|
+
"application/zip",
|
|
867
|
+
"application/gzip",
|
|
868
|
+
"application/x-tar",
|
|
869
|
+
"application/x-rar-compressed",
|
|
870
|
+
"application/x-7z-compressed",
|
|
871
|
+
"image/",
|
|
872
|
+
"video/",
|
|
873
|
+
"audio/",
|
|
874
|
+
"application/vnd.ms-excel",
|
|
875
|
+
"application/vnd.openxmlformats-officedocument",
|
|
876
|
+
"application/msword",
|
|
877
|
+
"application/vnd.ms-powerpoint"
|
|
878
|
+
];
|
|
879
|
+
return binaryTypes.some((type) => contentType.includes(type));
|
|
880
|
+
}
|
|
659
881
|
/**
|
|
660
882
|
* @description CORS middleware.
|
|
661
883
|
*/
|
|
662
884
|
setCorsHeaders(res, req) {
|
|
663
885
|
const origin = req.headers.origin;
|
|
664
886
|
const { allowedDomains = ["*"] } = this.config;
|
|
665
|
-
if (!origin || allowedDomains.length === 0)
|
|
666
|
-
|
|
887
|
+
if (!origin || allowedDomains.length === 0)
|
|
888
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
889
|
+
else if (allowedDomains.includes("*"))
|
|
890
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
667
891
|
else if (allowedDomains.includes(origin)) {
|
|
668
892
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
669
893
|
res.setHeader("Vary", "Origin");
|
|
670
894
|
}
|
|
671
|
-
res.setHeader(
|
|
672
|
-
|
|
895
|
+
res.setHeader(
|
|
896
|
+
"Access-Control-Allow-Methods",
|
|
897
|
+
"GET, POST, PUT, DELETE, PATCH, OPTIONS"
|
|
898
|
+
);
|
|
899
|
+
res.setHeader(
|
|
900
|
+
"Access-Control-Allow-Headers",
|
|
901
|
+
"Content-Type, Authorization"
|
|
902
|
+
);
|
|
673
903
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
674
904
|
}
|
|
675
905
|
/**
|
|
@@ -712,11 +942,15 @@ var MikroServe = class {
|
|
|
712
942
|
else if (typeof response.body === "string") res.end(response.body);
|
|
713
943
|
else res.end(JSON.stringify(response.body));
|
|
714
944
|
} else {
|
|
715
|
-
console.warn(
|
|
945
|
+
console.warn(
|
|
946
|
+
"Unexpected response object type without writeHead/end methods"
|
|
947
|
+
);
|
|
716
948
|
res.writeHead?.(response.statusCode, headers);
|
|
717
|
-
if (response.body === null || response.body === void 0)
|
|
949
|
+
if (response.body === null || response.body === void 0)
|
|
950
|
+
res.end?.();
|
|
718
951
|
else if (response.isRaw) res.end?.(response.body);
|
|
719
|
-
else if (typeof response.body === "string")
|
|
952
|
+
else if (typeof response.body === "string")
|
|
953
|
+
res.end?.(response.body);
|
|
720
954
|
else res.end?.(JSON.stringify(response.body));
|
|
721
955
|
}
|
|
722
956
|
}
|
|
@@ -727,15 +961,46 @@ var MikroServe = class {
|
|
|
727
961
|
const shutdown = (error) => {
|
|
728
962
|
console.log("Shutting down MikroServe server...");
|
|
729
963
|
if (error) console.error("Error:", error);
|
|
964
|
+
this.cleanupShutdownHandlers();
|
|
730
965
|
server.close(() => {
|
|
731
966
|
console.log("Server closed successfully");
|
|
732
|
-
|
|
967
|
+
if (process.env.NODE_ENV !== "test" && process.env.VITEST !== "true") {
|
|
968
|
+
setImmediate(() => process.exit(error ? 1 : 0));
|
|
969
|
+
}
|
|
733
970
|
});
|
|
734
971
|
};
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
972
|
+
const sigintHandler = () => shutdown();
|
|
973
|
+
const sigtermHandler = () => shutdown();
|
|
974
|
+
const uncaughtExceptionHandler = (error) => shutdown(error);
|
|
975
|
+
const unhandledRejectionHandler = (error) => shutdown(error);
|
|
976
|
+
this.shutdownHandlers = [
|
|
977
|
+
sigintHandler,
|
|
978
|
+
sigtermHandler,
|
|
979
|
+
uncaughtExceptionHandler,
|
|
980
|
+
unhandledRejectionHandler
|
|
981
|
+
];
|
|
982
|
+
process.on("SIGINT", sigintHandler);
|
|
983
|
+
process.on("SIGTERM", sigtermHandler);
|
|
984
|
+
process.on("uncaughtException", uncaughtExceptionHandler);
|
|
985
|
+
process.on("unhandledRejection", unhandledRejectionHandler);
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* @description Cleans up shutdown event listeners to prevent memory leaks.
|
|
989
|
+
*/
|
|
990
|
+
cleanupShutdownHandlers() {
|
|
991
|
+
if (this.shutdownHandlers.length > 0) {
|
|
992
|
+
const [
|
|
993
|
+
sigintHandler,
|
|
994
|
+
sigtermHandler,
|
|
995
|
+
uncaughtExceptionHandler,
|
|
996
|
+
unhandledRejectionHandler
|
|
997
|
+
] = this.shutdownHandlers;
|
|
998
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
999
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
1000
|
+
process.removeListener("uncaughtException", uncaughtExceptionHandler);
|
|
1001
|
+
process.removeListener("unhandledRejection", unhandledRejectionHandler);
|
|
1002
|
+
this.shutdownHandlers = [];
|
|
1003
|
+
}
|
|
739
1004
|
}
|
|
740
1005
|
};
|
|
741
1006
|
// Annotate the CommonJS export names for ESM import in node:
|
package/lib/index.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
|
};
|
|
@@ -49,6 +49,16 @@ type MikroServeConfiguration = {
|
|
|
49
49
|
* @default false
|
|
50
50
|
*/
|
|
51
51
|
debug: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum request body size in bytes.
|
|
54
|
+
* @default 1048576 (1MB)
|
|
55
|
+
*/
|
|
56
|
+
maxBodySize: number;
|
|
57
|
+
/**
|
|
58
|
+
* Request timeout in milliseconds. Set to 0 to disable.
|
|
59
|
+
* @default 30000 (30 seconds)
|
|
60
|
+
*/
|
|
61
|
+
requestTimeout: number;
|
|
52
62
|
/**
|
|
53
63
|
* Rate limiter settings.
|
|
54
64
|
*/
|
|
@@ -49,6 +49,16 @@ type MikroServeConfiguration = {
|
|
|
49
49
|
* @default false
|
|
50
50
|
*/
|
|
51
51
|
debug: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Maximum request body size in bytes.
|
|
54
|
+
* @default 1048576 (1MB)
|
|
55
|
+
*/
|
|
56
|
+
maxBodySize: number;
|
|
57
|
+
/**
|
|
58
|
+
* Request timeout in milliseconds. Set to 0 to disable.
|
|
59
|
+
* @default 30000 (30 seconds)
|
|
60
|
+
*/
|
|
61
|
+
requestTimeout: number;
|
|
52
62
|
/**
|
|
53
63
|
* Rate limiter settings.
|
|
54
64
|
*/
|
|
@@ -34,6 +34,10 @@ var configDefaults = () => {
|
|
|
34
34
|
sslKey: "",
|
|
35
35
|
sslCa: "",
|
|
36
36
|
debug: getTruthyValue(process.env.DEBUG) || false,
|
|
37
|
+
maxBodySize: 1024 * 1024,
|
|
38
|
+
// 1MB
|
|
39
|
+
requestTimeout: 3e4,
|
|
40
|
+
// 30 seconds
|
|
37
41
|
rateLimit: {
|
|
38
42
|
enabled: true,
|
|
39
43
|
requestsPerMinute: 100
|
|
@@ -52,6 +56,8 @@ var getDefaultConfig = () => {
|
|
|
52
56
|
sslKey: defaults.sslKey,
|
|
53
57
|
sslCa: defaults.sslCa,
|
|
54
58
|
debug: defaults.debug,
|
|
59
|
+
maxBodySize: defaults.maxBodySize,
|
|
60
|
+
requestTimeout: defaults.requestTimeout,
|
|
55
61
|
rateLimit: {
|
|
56
62
|
enabled: defaults.rateLimit.enabled,
|
|
57
63
|
requestsPerMinute: defaults.rateLimit.requestsPerMinute
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Multipart form-data parser for file uploads and form fields.
|
|
3
|
+
*/
|
|
4
|
+
interface MultipartFile {
|
|
5
|
+
filename: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
data: Buffer;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
interface MultipartFormData {
|
|
11
|
+
fields: Record<string, string | string[]>;
|
|
12
|
+
files: Record<string, MultipartFile | MultipartFile[]>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @description Parses multipart/form-data from a Buffer.
|
|
16
|
+
*/
|
|
17
|
+
declare function parseMultipartFormData(body: Buffer, contentType: string): MultipartFormData;
|
|
18
|
+
|
|
19
|
+
export { type MultipartFile, type MultipartFormData, parseMultipartFormData };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Multipart form-data parser for file uploads and form fields.
|
|
3
|
+
*/
|
|
4
|
+
interface MultipartFile {
|
|
5
|
+
filename: string;
|
|
6
|
+
contentType: string;
|
|
7
|
+
data: Buffer;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
interface MultipartFormData {
|
|
11
|
+
fields: Record<string, string | string[]>;
|
|
12
|
+
files: Record<string, MultipartFile | MultipartFile[]>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* @description Parses multipart/form-data from a Buffer.
|
|
16
|
+
*/
|
|
17
|
+
declare function parseMultipartFormData(body: Buffer, contentType: string): MultipartFormData;
|
|
18
|
+
|
|
19
|
+
export { type MultipartFile, type MultipartFormData, parseMultipartFormData };
|