mikroserve 0.0.2 → 0.0.4
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 +178 -1
- package/lib/MikroServe.d.mts +21 -41
- package/lib/MikroServe.d.ts +21 -41
- package/lib/MikroServe.js +102 -78
- package/lib/MikroServe.mjs +3 -1
- package/lib/{chunk-GGGGATKH.mjs → chunk-6UXWR6LD.mjs} +81 -78
- package/lib/chunk-CQPU7577.mjs +9 -0
- package/lib/chunk-RP67R3W4.mjs +43 -0
- package/lib/index.d.mts +2 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +118 -78
- package/lib/index.mjs +19 -1
- package/lib/interfaces/index.d.mts +71 -207
- package/lib/interfaces/index.d.ts +71 -207
- package/lib/utils/configDefaults.d.mts +20 -0
- package/lib/utils/configDefaults.d.ts +20 -0
- package/lib/utils/configDefaults.js +72 -0
- package/lib/utils/configDefaults.mjs +9 -0
- package/lib/utils/getTruthyEnvValue.d.mts +6 -0
- package/lib/utils/getTruthyEnvValue.d.ts +6 -0
- package/lib/utils/getTruthyEnvValue.js +33 -0
- package/lib/utils/getTruthyEnvValue.mjs +6 -0
- package/package.json +5 -3
package/lib/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
"use strict";
|
|
2
3
|
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
@@ -38,6 +39,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
38
39
|
var import_node_fs = require("fs");
|
|
39
40
|
var import_node_http = __toESM(require("http"));
|
|
40
41
|
var import_node_https = __toESM(require("https"));
|
|
42
|
+
var import_mikroconf = require("mikroconf");
|
|
41
43
|
|
|
42
44
|
// src/RateLimiter.ts
|
|
43
45
|
var RateLimiter = class {
|
|
@@ -282,93 +284,137 @@ var Router = class {
|
|
|
282
284
|
}
|
|
283
285
|
};
|
|
284
286
|
|
|
287
|
+
// src/utils/getTruthyEnvValue.ts
|
|
288
|
+
function getTruthyEnvValue(value) {
|
|
289
|
+
if (value === "true" || value === true) return true;
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/utils/configDefaults.ts
|
|
294
|
+
var configDefaults = () => {
|
|
295
|
+
return {
|
|
296
|
+
port: Number(process.env.PORT) || 3e3,
|
|
297
|
+
host: process.env.HOST || "0.0.0.0",
|
|
298
|
+
useHttps: false,
|
|
299
|
+
sslCert: "",
|
|
300
|
+
sslKey: "",
|
|
301
|
+
sslCa: "",
|
|
302
|
+
debug: getTruthyEnvValue(process.env.DEBUG) || false,
|
|
303
|
+
rateLimit: {
|
|
304
|
+
enabled: true,
|
|
305
|
+
requestsPerMinute: 100
|
|
306
|
+
},
|
|
307
|
+
allowedDomains: ["*"]
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
285
311
|
// src/MikroServe.ts
|
|
286
312
|
var MikroServe = class {
|
|
287
|
-
config;
|
|
288
313
|
rateLimiter;
|
|
289
314
|
router;
|
|
290
|
-
|
|
291
|
-
sslCert;
|
|
292
|
-
sslKey;
|
|
293
|
-
sslCa;
|
|
294
|
-
debug;
|
|
315
|
+
config;
|
|
295
316
|
/**
|
|
296
317
|
* @description Creates a new MikroServe instance.
|
|
297
|
-
* @param config - Server configuration options
|
|
298
318
|
*/
|
|
299
|
-
constructor(
|
|
319
|
+
constructor(options) {
|
|
320
|
+
const defaults = configDefaults();
|
|
321
|
+
const config = new import_mikroconf.MikroConf({
|
|
322
|
+
configFilePath: "mikroserve.config.json",
|
|
323
|
+
args: process.argv,
|
|
324
|
+
options: [
|
|
325
|
+
{ flag: "--port", path: "port", defaultValue: defaults.port },
|
|
326
|
+
{ flag: "--host", path: "host", defaultValue: defaults.host },
|
|
327
|
+
{ flag: "--https", path: "useHttps", defaultValue: defaults.useHttps, isFlag: true },
|
|
328
|
+
{ flag: "--cert", path: "sslCert", defaultValue: defaults.sslCert },
|
|
329
|
+
{ flag: "--key", path: "sslKey", defaultValue: defaults.sslKey },
|
|
330
|
+
{ flag: "--ca", path: "sslCa", defaultValue: defaults.sslCa },
|
|
331
|
+
{
|
|
332
|
+
flag: "--ratelimit",
|
|
333
|
+
path: "rateLimit.enabled",
|
|
334
|
+
defaultValue: defaults.rateLimit.enabled,
|
|
335
|
+
isFlag: true
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
flag: "--rps",
|
|
339
|
+
path: "rateLimit.requestsPerMinute",
|
|
340
|
+
defaultValue: defaults.rateLimit.requestsPerMinute
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
flag: "--allowed",
|
|
344
|
+
path: "allowedDomains",
|
|
345
|
+
defaultValue: defaults.allowedDomains,
|
|
346
|
+
parser: import_mikroconf.parsers.array
|
|
347
|
+
},
|
|
348
|
+
{ flag: "--debug", path: "debug", defaultValue: defaults.debug, isFlag: true }
|
|
349
|
+
],
|
|
350
|
+
config: options
|
|
351
|
+
}).get();
|
|
352
|
+
if (config.debug) console.log("Using configuration:", config);
|
|
300
353
|
this.config = config;
|
|
301
|
-
const { useHttps, sslCa, sslCert, sslKey, debug } = config;
|
|
302
|
-
const requestsPerMinute = config.rateLimit?.requestsPerMinute || 50;
|
|
303
|
-
this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
|
|
304
354
|
this.router = new Router();
|
|
305
|
-
|
|
306
|
-
this.
|
|
307
|
-
|
|
308
|
-
this.sslCa = sslCa;
|
|
309
|
-
this.debug = debug || false;
|
|
310
|
-
if (config.rateLimit?.enabled !== false) this.use(this.rateLimitMiddleware.bind(this));
|
|
355
|
+
const requestsPerMinute = config.rateLimit.requestsPerMinute || defaults.rateLimit.requestsPerMinute;
|
|
356
|
+
this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
|
|
357
|
+
if (config.rateLimit.enabled === true) this.use(this.rateLimitMiddleware.bind(this));
|
|
311
358
|
}
|
|
312
359
|
/**
|
|
313
|
-
* Register a global middleware
|
|
360
|
+
* @description Register a global middleware.
|
|
314
361
|
*/
|
|
315
362
|
use(middleware) {
|
|
316
363
|
this.router.use(middleware);
|
|
317
364
|
return this;
|
|
318
365
|
}
|
|
319
366
|
/**
|
|
320
|
-
* Register a GET route
|
|
367
|
+
* @description Register a GET route.
|
|
321
368
|
*/
|
|
322
369
|
get(path, ...handlers) {
|
|
323
370
|
this.router.get(path, ...handlers);
|
|
324
371
|
return this;
|
|
325
372
|
}
|
|
326
373
|
/**
|
|
327
|
-
* Register a POST route
|
|
374
|
+
* @description Register a POST route.
|
|
328
375
|
*/
|
|
329
376
|
post(path, ...handlers) {
|
|
330
377
|
this.router.post(path, ...handlers);
|
|
331
378
|
return this;
|
|
332
379
|
}
|
|
333
380
|
/**
|
|
334
|
-
* Register a PUT route
|
|
381
|
+
* @description Register a PUT route.
|
|
335
382
|
*/
|
|
336
383
|
put(path, ...handlers) {
|
|
337
384
|
this.router.put(path, ...handlers);
|
|
338
385
|
return this;
|
|
339
386
|
}
|
|
340
387
|
/**
|
|
341
|
-
* Register a DELETE route
|
|
388
|
+
* @description Register a DELETE route.
|
|
342
389
|
*/
|
|
343
390
|
delete(path, ...handlers) {
|
|
344
391
|
this.router.delete(path, ...handlers);
|
|
345
392
|
return this;
|
|
346
393
|
}
|
|
347
394
|
/**
|
|
348
|
-
* Register a PATCH route
|
|
395
|
+
* @description Register a PATCH route.
|
|
349
396
|
*/
|
|
350
397
|
patch(path, ...handlers) {
|
|
351
398
|
this.router.patch(path, ...handlers);
|
|
352
399
|
return this;
|
|
353
400
|
}
|
|
354
401
|
/**
|
|
355
|
-
* Register an OPTIONS route
|
|
402
|
+
* @description Register an OPTIONS route.
|
|
356
403
|
*/
|
|
357
404
|
options(path, ...handlers) {
|
|
358
405
|
this.router.options(path, ...handlers);
|
|
359
406
|
return this;
|
|
360
407
|
}
|
|
361
408
|
/**
|
|
362
|
-
* Creates an HTTP/HTTPS server, sets up graceful shutdown, and starts listening.
|
|
363
|
-
* @returns Running HTTP/HTTPS server
|
|
409
|
+
* @description Creates an HTTP/HTTPS server, sets up graceful shutdown, and starts listening.
|
|
364
410
|
*/
|
|
365
411
|
start() {
|
|
366
412
|
const server = this.createServer();
|
|
367
|
-
const { port
|
|
413
|
+
const { port, host } = this.config;
|
|
368
414
|
this.setupGracefulShutdown(server);
|
|
369
415
|
server.listen(port, host, () => {
|
|
370
416
|
const address = server.address();
|
|
371
|
-
const protocol = this.useHttps ? "https" : "http";
|
|
417
|
+
const protocol = this.config.useHttps ? "https" : "http";
|
|
372
418
|
console.log(
|
|
373
419
|
`MikroServe running at ${protocol}://${address.address !== "::" ? address.address : "localhost"}:${address.port}`
|
|
374
420
|
);
|
|
@@ -376,19 +422,18 @@ var MikroServe = class {
|
|
|
376
422
|
return server;
|
|
377
423
|
}
|
|
378
424
|
/**
|
|
379
|
-
* Creates and configures a server instance without starting it.
|
|
380
|
-
* @returns Configured HTTP or HTTPS server instance
|
|
425
|
+
* @description Creates and configures a server instance without starting it.
|
|
381
426
|
*/
|
|
382
427
|
createServer() {
|
|
383
428
|
const boundRequestHandler = this.requestHandler.bind(this);
|
|
384
|
-
if (this.useHttps) {
|
|
385
|
-
if (!this.sslCert || !this.sslKey)
|
|
429
|
+
if (this.config.useHttps) {
|
|
430
|
+
if (!this.config.sslCert || !this.config.sslKey)
|
|
386
431
|
throw new Error("SSL certificate and key paths are required when useHttps is true");
|
|
387
432
|
try {
|
|
388
433
|
const httpsOptions = {
|
|
389
|
-
key: (0, import_node_fs.readFileSync)(this.sslKey),
|
|
390
|
-
cert: (0, import_node_fs.readFileSync)(this.sslCert),
|
|
391
|
-
...this.sslCa ? { ca: (0, import_node_fs.readFileSync)(this.sslCa) } : {}
|
|
434
|
+
key: (0, import_node_fs.readFileSync)(this.config.sslKey),
|
|
435
|
+
cert: (0, import_node_fs.readFileSync)(this.config.sslCert),
|
|
436
|
+
...this.config.sslCa ? { ca: (0, import_node_fs.readFileSync)(this.config.sslCa) } : {}
|
|
392
437
|
};
|
|
393
438
|
return import_node_https.default.createServer(httpsOptions, boundRequestHandler);
|
|
394
439
|
} catch (error) {
|
|
@@ -400,7 +445,7 @@ var MikroServe = class {
|
|
|
400
445
|
return import_node_http.default.createServer(boundRequestHandler);
|
|
401
446
|
}
|
|
402
447
|
/**
|
|
403
|
-
* Rate limiting middleware
|
|
448
|
+
* @description Rate limiting middleware.
|
|
404
449
|
*/
|
|
405
450
|
async rateLimitMiddleware(context, next) {
|
|
406
451
|
const ip = context.req.socket.remoteAddress || "unknown";
|
|
@@ -423,16 +468,17 @@ var MikroServe = class {
|
|
|
423
468
|
return next();
|
|
424
469
|
}
|
|
425
470
|
/**
|
|
426
|
-
* Request handler for HTTP and HTTPS servers.
|
|
471
|
+
* @description Request handler for HTTP and HTTPS servers.
|
|
427
472
|
*/
|
|
428
473
|
async requestHandler(req, res) {
|
|
429
474
|
const start = Date.now();
|
|
430
475
|
const method = req.method || "UNKNOWN";
|
|
431
476
|
const url = req.url || "/unknown";
|
|
477
|
+
const isDebug = this.config.debug;
|
|
432
478
|
try {
|
|
433
479
|
this.setCorsHeaders(res, req);
|
|
434
|
-
this.setSecurityHeaders(res, this.useHttps);
|
|
435
|
-
if (
|
|
480
|
+
this.setSecurityHeaders(res, this.config.useHttps);
|
|
481
|
+
if (isDebug) console.log(`${method} ${url}`);
|
|
436
482
|
if (req.method === "OPTIONS") {
|
|
437
483
|
res.statusCode = 204;
|
|
438
484
|
res.end();
|
|
@@ -441,9 +487,7 @@ var MikroServe = class {
|
|
|
441
487
|
try {
|
|
442
488
|
req.body = await this.parseBody(req);
|
|
443
489
|
} catch (error) {
|
|
444
|
-
if (
|
|
445
|
-
console.error("Body parsing error:", error.message);
|
|
446
|
-
}
|
|
490
|
+
if (isDebug) console.error("Body parsing error:", error.message);
|
|
447
491
|
return this.respond(res, {
|
|
448
492
|
statusCode: 400,
|
|
449
493
|
body: {
|
|
@@ -467,33 +511,22 @@ var MikroServe = class {
|
|
|
467
511
|
statusCode: 500,
|
|
468
512
|
body: {
|
|
469
513
|
error: "Internal Server Error",
|
|
470
|
-
message:
|
|
514
|
+
message: isDebug ? error.message : "An unexpected error occurred"
|
|
471
515
|
}
|
|
472
516
|
});
|
|
473
517
|
} finally {
|
|
474
|
-
if (this.
|
|
475
|
-
this.logDuration(start, method, url);
|
|
476
|
-
}
|
|
518
|
+
if (isDebug) this.logDuration(start, method, url);
|
|
477
519
|
}
|
|
478
520
|
}
|
|
479
521
|
/**
|
|
480
|
-
* Writes out a clean log to represent the duration of the request.
|
|
522
|
+
* @description Writes out a clean log to represent the duration of the request.
|
|
481
523
|
*/
|
|
482
524
|
logDuration(start, method, url) {
|
|
483
525
|
const duration = Date.now() - start;
|
|
484
526
|
console.log(`${method} ${url} completed in ${duration}ms`);
|
|
485
527
|
}
|
|
486
528
|
/**
|
|
487
|
-
* Parses the request body based on content type.
|
|
488
|
-
* @param req - HTTP request object
|
|
489
|
-
* @returns Promise resolving to the parsed body
|
|
490
|
-
* @throws Will throw if body cannot be parsed
|
|
491
|
-
*/
|
|
492
|
-
/**
|
|
493
|
-
* Parses the request body based on content type.
|
|
494
|
-
* @param req - HTTP request object
|
|
495
|
-
* @returns Promise resolving to the parsed body
|
|
496
|
-
* @throws Will throw if body cannot be parsed
|
|
529
|
+
* @description Parses the request body based on content type.
|
|
497
530
|
*/
|
|
498
531
|
async parseBody(req) {
|
|
499
532
|
return new Promise((resolve, reject) => {
|
|
@@ -501,17 +534,17 @@ var MikroServe = class {
|
|
|
501
534
|
let bodySize = 0;
|
|
502
535
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
503
536
|
let rejected = false;
|
|
537
|
+
const isDebug = this.config.debug;
|
|
504
538
|
const contentType = req.headers["content-type"] || "";
|
|
505
|
-
if (
|
|
539
|
+
if (isDebug) {
|
|
506
540
|
console.log("Content-Type:", contentType);
|
|
507
541
|
}
|
|
508
542
|
req.on("data", (chunk) => {
|
|
509
543
|
bodySize += chunk.length;
|
|
510
|
-
if (
|
|
511
|
-
console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
|
|
544
|
+
if (isDebug) console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
|
|
512
545
|
if (bodySize > MAX_BODY_SIZE && !rejected) {
|
|
513
546
|
rejected = true;
|
|
514
|
-
if (
|
|
547
|
+
if (isDebug) console.log(`Body size exceeded limit: ${bodySize} > ${MAX_BODY_SIZE}`);
|
|
515
548
|
reject(new Error("Request body too large"));
|
|
516
549
|
return;
|
|
517
550
|
}
|
|
@@ -519,7 +552,7 @@ var MikroServe = class {
|
|
|
519
552
|
});
|
|
520
553
|
req.on("end", () => {
|
|
521
554
|
if (rejected) return;
|
|
522
|
-
if (
|
|
555
|
+
if (isDebug) console.log(`Request body complete: ${bodySize} bytes`);
|
|
523
556
|
try {
|
|
524
557
|
if (bodyChunks.length > 0) {
|
|
525
558
|
const bodyString = Buffer.concat(bodyChunks).toString("utf8");
|
|
@@ -551,17 +584,14 @@ var MikroServe = class {
|
|
|
551
584
|
});
|
|
552
585
|
}
|
|
553
586
|
/**
|
|
554
|
-
* CORS middleware
|
|
587
|
+
* @description CORS middleware.
|
|
555
588
|
*/
|
|
556
|
-
// Update the setCorsHeaders method in MikroServe class to handle allowed domains
|
|
557
589
|
setCorsHeaders(res, req) {
|
|
558
590
|
const origin = req.headers.origin;
|
|
559
591
|
const { allowedDomains = ["*"] } = this.config;
|
|
560
|
-
if (!origin || allowedDomains.length === 0)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
564
|
-
} else if (allowedDomains.includes(origin)) {
|
|
592
|
+
if (!origin || allowedDomains.length === 0) res.setHeader("Access-Control-Allow-Origin", "*");
|
|
593
|
+
else if (allowedDomains.includes("*")) res.setHeader("Access-Control-Allow-Origin", "*");
|
|
594
|
+
else if (allowedDomains.includes(origin)) {
|
|
565
595
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
566
596
|
res.setHeader("Vary", "Origin");
|
|
567
597
|
}
|
|
@@ -570,7 +600,7 @@ var MikroServe = class {
|
|
|
570
600
|
res.setHeader("Access-Control-Max-Age", "86400");
|
|
571
601
|
}
|
|
572
602
|
/**
|
|
573
|
-
* Set security headers
|
|
603
|
+
* @description Set security headers.
|
|
574
604
|
*/
|
|
575
605
|
setSecurityHeaders(res, isHttps = false) {
|
|
576
606
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
@@ -583,9 +613,7 @@ var MikroServe = class {
|
|
|
583
613
|
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
584
614
|
}
|
|
585
615
|
/**
|
|
586
|
-
* Sends a response with appropriate headers.
|
|
587
|
-
* @param res - HTTP response object
|
|
588
|
-
* @param response - Response data and status code
|
|
616
|
+
* @description Sends a response with appropriate headers.
|
|
589
617
|
*/
|
|
590
618
|
respond(res, response) {
|
|
591
619
|
const headers = {
|
|
@@ -597,8 +625,7 @@ var MikroServe = class {
|
|
|
597
625
|
else res.end(JSON.stringify(response.body));
|
|
598
626
|
}
|
|
599
627
|
/**
|
|
600
|
-
* Sets up graceful shutdown handlers for a server.
|
|
601
|
-
* @param server - The HTTP/HTTPS server to add shutdown handlers to
|
|
628
|
+
* @description Sets up graceful shutdown handlers for a server.
|
|
602
629
|
*/
|
|
603
630
|
setupGracefulShutdown(server) {
|
|
604
631
|
const shutdown = (error) => {
|
|
@@ -606,9 +633,7 @@ var MikroServe = class {
|
|
|
606
633
|
if (error) console.error("Error:", error);
|
|
607
634
|
server.close(() => {
|
|
608
635
|
console.log("Server closed successfully");
|
|
609
|
-
setImmediate(() =>
|
|
610
|
-
process.exit(error ? 1 : 0);
|
|
611
|
-
});
|
|
636
|
+
setImmediate(() => process.exit(error ? 1 : 0));
|
|
612
637
|
});
|
|
613
638
|
};
|
|
614
639
|
process.on("SIGINT", () => shutdown());
|
|
@@ -617,6 +642,21 @@ var MikroServe = class {
|
|
|
617
642
|
process.on("unhandledRejection", shutdown);
|
|
618
643
|
}
|
|
619
644
|
};
|
|
645
|
+
|
|
646
|
+
// src/index.ts
|
|
647
|
+
async function main() {
|
|
648
|
+
const isRunFromCommandLine = process.argv[1]?.includes("node_modules/.bin/mikroserve");
|
|
649
|
+
const force = process.argv[1]?.includes("mikroauth/src/index.ts") && (process.argv[2] || "") === "--force";
|
|
650
|
+
if (isRunFromCommandLine || force && isRunFromCommandLine) {
|
|
651
|
+
console.log("\u{1F680} Welcome to MikroServe! \u2728");
|
|
652
|
+
try {
|
|
653
|
+
new MikroServe();
|
|
654
|
+
} catch (error) {
|
|
655
|
+
console.error(error);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
main();
|
|
620
660
|
// Annotate the CommonJS export names for ESM import in node:
|
|
621
661
|
0 && (module.exports = {
|
|
622
662
|
MikroServe
|
package/lib/index.mjs
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
import {
|
|
2
3
|
MikroServe
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6UXWR6LD.mjs";
|
|
4
5
|
import "./chunk-ZFBBESGU.mjs";
|
|
5
6
|
import "./chunk-KJT4SET2.mjs";
|
|
7
|
+
import "./chunk-RP67R3W4.mjs";
|
|
8
|
+
import "./chunk-CQPU7577.mjs";
|
|
9
|
+
|
|
10
|
+
// src/index.ts
|
|
11
|
+
async function main() {
|
|
12
|
+
const isRunFromCommandLine = process.argv[1]?.includes("node_modules/.bin/mikroserve");
|
|
13
|
+
const force = process.argv[1]?.includes("mikroauth/src/index.ts") && (process.argv[2] || "") === "--force";
|
|
14
|
+
if (isRunFromCommandLine || force && isRunFromCommandLine) {
|
|
15
|
+
console.log("\u{1F680} Welcome to MikroServe! \u2728");
|
|
16
|
+
try {
|
|
17
|
+
new MikroServe();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error(error);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
main();
|
|
6
24
|
export {
|
|
7
25
|
MikroServe
|
|
8
26
|
};
|