mikroserve 0.0.1

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/index.js ADDED
@@ -0,0 +1,623 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ MikroServe: () => MikroServe
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/MikroServe.ts
38
+ var import_node_fs = require("fs");
39
+ var import_node_http = __toESM(require("http"));
40
+ var import_node_https = __toESM(require("https"));
41
+
42
+ // src/RateLimiter.ts
43
+ var RateLimiter = class {
44
+ requests = /* @__PURE__ */ new Map();
45
+ limit;
46
+ windowMs;
47
+ constructor(limit = 100, windowSeconds = 60) {
48
+ this.limit = limit;
49
+ this.windowMs = windowSeconds * 1e3;
50
+ setInterval(() => this.cleanup(), this.windowMs);
51
+ }
52
+ getLimit() {
53
+ return this.limit;
54
+ }
55
+ isAllowed(ip) {
56
+ const now = Date.now();
57
+ const key = ip || "unknown";
58
+ let entry = this.requests.get(key);
59
+ if (!entry || entry.resetTime < now) {
60
+ entry = { count: 0, resetTime: now + this.windowMs };
61
+ this.requests.set(key, entry);
62
+ }
63
+ entry.count++;
64
+ return entry.count <= this.limit;
65
+ }
66
+ getRemainingRequests(ip) {
67
+ const now = Date.now();
68
+ const key = ip || "unknown";
69
+ const entry = this.requests.get(key);
70
+ if (!entry || entry.resetTime < now) return this.limit;
71
+ return Math.max(0, this.limit - entry.count);
72
+ }
73
+ getResetTime(ip) {
74
+ const now = Date.now();
75
+ const key = ip || "unknown";
76
+ const entry = this.requests.get(key);
77
+ if (!entry || entry.resetTime < now) return Math.floor((now + this.windowMs) / 1e3);
78
+ return Math.floor(entry.resetTime / 1e3);
79
+ }
80
+ cleanup() {
81
+ const now = Date.now();
82
+ for (const [key, entry] of this.requests.entries()) {
83
+ if (entry.resetTime < now) this.requests.delete(key);
84
+ }
85
+ }
86
+ };
87
+
88
+ // src/Router.ts
89
+ var import_node_url = require("url");
90
+ var Router = class {
91
+ routes = [];
92
+ globalMiddlewares = [];
93
+ pathPatterns = /* @__PURE__ */ new Map();
94
+ /**
95
+ * Add a global middleware
96
+ */
97
+ use(middleware) {
98
+ this.globalMiddlewares.push(middleware);
99
+ return this;
100
+ }
101
+ /**
102
+ * Register a route with specified method
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
111
+ */
112
+ get(path, ...handlers) {
113
+ const handler = handlers.pop();
114
+ return this.register("GET", path, handler, handlers);
115
+ }
116
+ /**
117
+ * Register a POST route
118
+ */
119
+ post(path, ...handlers) {
120
+ const handler = handlers.pop();
121
+ return this.register("POST", path, handler, handlers);
122
+ }
123
+ /**
124
+ * Register a PUT route
125
+ */
126
+ put(path, ...handlers) {
127
+ const handler = handlers.pop();
128
+ return this.register("PUT", path, handler, handlers);
129
+ }
130
+ /**
131
+ * Register a DELETE route
132
+ */
133
+ delete(path, ...handlers) {
134
+ const handler = handlers.pop();
135
+ return this.register("DELETE", path, handler, handlers);
136
+ }
137
+ /**
138
+ * Register a PATCH route
139
+ */
140
+ patch(path, ...handlers) {
141
+ const handler = handlers.pop();
142
+ return this.register("PATCH", path, handler, handlers);
143
+ }
144
+ /**
145
+ * Register an OPTIONS route
146
+ */
147
+ options(path, ...handlers) {
148
+ const handler = handlers.pop();
149
+ return this.register("OPTIONS", path, handler, handlers);
150
+ }
151
+ /**
152
+ * Match a request to a route
153
+ */
154
+ match(method, path) {
155
+ for (const route of this.routes) {
156
+ if (route.method !== method) continue;
157
+ const pathPattern = this.pathPatterns.get(route.path);
158
+ if (!pathPattern) continue;
159
+ const match = pathPattern.pattern.exec(path);
160
+ if (!match) continue;
161
+ const params = {};
162
+ pathPattern.paramNames.forEach((name, index) => {
163
+ params[name] = match[index + 1] || "";
164
+ });
165
+ return { route, params };
166
+ }
167
+ return null;
168
+ }
169
+ /**
170
+ * Create a regex pattern for path matching
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
186
+ */
187
+ async handle(req, res) {
188
+ const method = req.method || "GET";
189
+ const url = new import_node_url.URL(req.url || "/", `http://${req.headers.host}`);
190
+ const path = url.pathname;
191
+ const matched = this.match(method, path);
192
+ if (!matched) return null;
193
+ const { route, params } = matched;
194
+ const query = {};
195
+ url.searchParams.forEach((value, key) => {
196
+ query[key] = value;
197
+ });
198
+ const context = {
199
+ req,
200
+ res,
201
+ params,
202
+ query,
203
+ // @ts-ignore
204
+ body: req.body || {},
205
+ headers: req.headers,
206
+ path,
207
+ state: {},
208
+ // Add the missing state property
209
+ text: (content, status = 200) => ({
210
+ statusCode: status,
211
+ body: content,
212
+ headers: { "Content-Type": "text/plain" }
213
+ }),
214
+ form: (content, status = 200) => ({
215
+ statusCode: status,
216
+ body: content,
217
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
218
+ }),
219
+ json: (content, status = 200) => ({
220
+ statusCode: status,
221
+ body: content,
222
+ headers: { "Content-Type": "application/json" }
223
+ }),
224
+ html: (content, status = 200) => ({
225
+ statusCode: status,
226
+ body: content,
227
+ headers: { "Content-Type": "text/html" }
228
+ }),
229
+ redirect: (url2, status = 302) => ({
230
+ statusCode: status,
231
+ body: null,
232
+ headers: { Location: url2 }
233
+ }),
234
+ status: function(code) {
235
+ return {
236
+ text: (content) => ({
237
+ statusCode: code,
238
+ body: content,
239
+ headers: { "Content-Type": "text/plain" }
240
+ }),
241
+ json: (data) => ({
242
+ statusCode: code,
243
+ body: data,
244
+ headers: { "Content-Type": "application/json" }
245
+ }),
246
+ html: (content) => ({
247
+ statusCode: code,
248
+ body: content,
249
+ headers: { "Content-Type": "text/html" }
250
+ }),
251
+ form: (content) => ({
252
+ // Make sure form method is included here
253
+ statusCode: code,
254
+ body: content,
255
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
256
+ }),
257
+ redirect: (url2, redirectStatus = 302) => ({
258
+ statusCode: redirectStatus,
259
+ body: null,
260
+ headers: { Location: url2 }
261
+ }),
262
+ status: (updatedCode) => this.status(updatedCode)
263
+ };
264
+ }
265
+ };
266
+ const middlewares = [...this.globalMiddlewares, ...route.middlewares];
267
+ return this.executeMiddlewareChain(context, middlewares, route.handler);
268
+ }
269
+ /**
270
+ * Execute middleware chain and final handler
271
+ */
272
+ async executeMiddlewareChain(context, middlewares, finalHandler) {
273
+ let currentIndex = 0;
274
+ const next = async () => {
275
+ if (currentIndex < middlewares.length) {
276
+ const middleware = middlewares[currentIndex++];
277
+ return middleware(context, next);
278
+ }
279
+ return finalHandler(context);
280
+ };
281
+ return next();
282
+ }
283
+ };
284
+
285
+ // src/MikroServe.ts
286
+ var MikroServe = class {
287
+ config;
288
+ rateLimiter;
289
+ router;
290
+ useHttps;
291
+ sslCert;
292
+ sslKey;
293
+ sslCa;
294
+ debug;
295
+ /**
296
+ * @description Creates a new MikroServe instance.
297
+ * @param config - Server configuration options
298
+ */
299
+ constructor(config) {
300
+ 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
+ this.router = new Router();
305
+ this.useHttps = useHttps || false;
306
+ this.sslCert = sslCert;
307
+ this.sslKey = sslKey;
308
+ this.sslCa = sslCa;
309
+ this.debug = debug || false;
310
+ if (config.rateLimit?.enabled !== false) this.use(this.rateLimitMiddleware.bind(this));
311
+ }
312
+ /**
313
+ * Register a global middleware
314
+ */
315
+ use(middleware) {
316
+ this.router.use(middleware);
317
+ return this;
318
+ }
319
+ /**
320
+ * Register a GET route
321
+ */
322
+ get(path, ...handlers) {
323
+ this.router.get(path, ...handlers);
324
+ return this;
325
+ }
326
+ /**
327
+ * Register a POST route
328
+ */
329
+ post(path, ...handlers) {
330
+ this.router.post(path, ...handlers);
331
+ return this;
332
+ }
333
+ /**
334
+ * Register a PUT route
335
+ */
336
+ put(path, ...handlers) {
337
+ this.router.put(path, ...handlers);
338
+ return this;
339
+ }
340
+ /**
341
+ * Register a DELETE route
342
+ */
343
+ delete(path, ...handlers) {
344
+ this.router.delete(path, ...handlers);
345
+ return this;
346
+ }
347
+ /**
348
+ * Register a PATCH route
349
+ */
350
+ patch(path, ...handlers) {
351
+ this.router.patch(path, ...handlers);
352
+ return this;
353
+ }
354
+ /**
355
+ * Register an OPTIONS route
356
+ */
357
+ options(path, ...handlers) {
358
+ this.router.options(path, ...handlers);
359
+ return this;
360
+ }
361
+ /**
362
+ * Creates an HTTP/HTTPS server, sets up graceful shutdown, and starts listening.
363
+ * @returns Running HTTP/HTTPS server
364
+ */
365
+ start() {
366
+ const server = this.createServer();
367
+ const { port = 3e3, host = "localhost" } = this.config;
368
+ this.setupGracefulShutdown(server);
369
+ server.listen(port, host, () => {
370
+ const address = server.address();
371
+ const protocol = this.useHttps ? "https" : "http";
372
+ console.log(
373
+ `MikroServe running at ${protocol}://${address.address !== "::" ? address.address : "localhost"}:${address.port}`
374
+ );
375
+ });
376
+ return server;
377
+ }
378
+ /**
379
+ * Creates and configures a server instance without starting it.
380
+ * @returns Configured HTTP or HTTPS server instance
381
+ */
382
+ createServer() {
383
+ const boundRequestHandler = this.requestHandler.bind(this);
384
+ if (this.useHttps) {
385
+ if (!this.sslCert || !this.sslKey)
386
+ throw new Error("SSL certificate and key paths are required when useHttps is true");
387
+ try {
388
+ 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) } : {}
392
+ };
393
+ return import_node_https.default.createServer(httpsOptions, boundRequestHandler);
394
+ } catch (error) {
395
+ if (error.message.includes("key values mismatch"))
396
+ throw new Error(`SSL certificate and key do not match: ${error.message}`);
397
+ throw error;
398
+ }
399
+ }
400
+ return import_node_http.default.createServer(boundRequestHandler);
401
+ }
402
+ /**
403
+ * Rate limiting middleware
404
+ */
405
+ async rateLimitMiddleware(context, next) {
406
+ const ip = context.req.socket.remoteAddress || "unknown";
407
+ context.res.setHeader("X-RateLimit-Limit", this.rateLimiter.getLimit().toString());
408
+ context.res.setHeader(
409
+ "X-RateLimit-Remaining",
410
+ this.rateLimiter.getRemainingRequests(ip).toString()
411
+ );
412
+ context.res.setHeader("X-RateLimit-Reset", this.rateLimiter.getResetTime(ip).toString());
413
+ if (!this.rateLimiter.isAllowed(ip)) {
414
+ return {
415
+ statusCode: 429,
416
+ body: {
417
+ error: "Too Many Requests",
418
+ message: "Rate limit exceeded, please try again later"
419
+ },
420
+ headers: { "Content-Type": "application/json" }
421
+ };
422
+ }
423
+ return next();
424
+ }
425
+ /**
426
+ * Request handler for HTTP and HTTPS servers.
427
+ */
428
+ async requestHandler(req, res) {
429
+ const start = Date.now();
430
+ const method = req.method || "UNKNOWN";
431
+ const url = req.url || "/unknown";
432
+ try {
433
+ this.setCorsHeaders(res, req);
434
+ this.setSecurityHeaders(res, this.useHttps);
435
+ if (this.debug) console.log(`${method} ${url}`);
436
+ if (req.method === "OPTIONS") {
437
+ res.statusCode = 204;
438
+ res.end();
439
+ return;
440
+ }
441
+ try {
442
+ req.body = await this.parseBody(req);
443
+ } catch (error) {
444
+ if (this.debug) {
445
+ console.error("Body parsing error:", error.message);
446
+ }
447
+ return this.respond(res, {
448
+ statusCode: 400,
449
+ body: {
450
+ error: "Bad Request",
451
+ message: error.message
452
+ }
453
+ });
454
+ }
455
+ const result = await this.router.handle(req, res);
456
+ if (result) return this.respond(res, result);
457
+ return this.respond(res, {
458
+ statusCode: 404,
459
+ body: {
460
+ error: "Not Found",
461
+ message: "The requested endpoint does not exist"
462
+ }
463
+ });
464
+ } catch (error) {
465
+ console.error("Server error:", error);
466
+ return this.respond(res, {
467
+ statusCode: 500,
468
+ body: {
469
+ error: "Internal Server Error",
470
+ message: this.debug ? error.message : "An unexpected error occurred"
471
+ }
472
+ });
473
+ } finally {
474
+ if (this.debug) {
475
+ this.logDuration(start, method, url);
476
+ }
477
+ }
478
+ }
479
+ /**
480
+ * Writes out a clean log to represent the duration of the request.
481
+ */
482
+ logDuration(start, method, url) {
483
+ const duration = Date.now() - start;
484
+ console.log(`${method} ${url} completed in ${duration}ms`);
485
+ }
486
+ /**
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
497
+ */
498
+ async parseBody(req) {
499
+ return new Promise((resolve, reject) => {
500
+ const bodyChunks = [];
501
+ let bodySize = 0;
502
+ const MAX_BODY_SIZE = 1024 * 1024;
503
+ let rejected = false;
504
+ const contentType = req.headers["content-type"] || "";
505
+ if (this.debug) {
506
+ console.log("Content-Type:", contentType);
507
+ }
508
+ req.on("data", (chunk) => {
509
+ bodySize += chunk.length;
510
+ if (this.debug)
511
+ console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
512
+ if (bodySize > MAX_BODY_SIZE && !rejected) {
513
+ rejected = true;
514
+ if (this.debug) console.log(`Body size exceeded limit: ${bodySize} > ${MAX_BODY_SIZE}`);
515
+ reject(new Error("Request body too large"));
516
+ return;
517
+ }
518
+ if (!rejected) bodyChunks.push(chunk);
519
+ });
520
+ req.on("end", () => {
521
+ if (rejected) return;
522
+ if (this.debug) console.log(`Request body complete: ${bodySize} bytes`);
523
+ try {
524
+ if (bodyChunks.length > 0) {
525
+ const bodyString = Buffer.concat(bodyChunks).toString("utf8");
526
+ if (contentType.includes("application/json")) {
527
+ try {
528
+ resolve(JSON.parse(bodyString));
529
+ } catch (error) {
530
+ reject(new Error(`Invalid JSON in request body: ${error.message}`));
531
+ }
532
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
533
+ const formData = {};
534
+ new URLSearchParams(bodyString).forEach((value, key) => {
535
+ formData[key] = value;
536
+ });
537
+ resolve(formData);
538
+ } else {
539
+ resolve(bodyString);
540
+ }
541
+ } else {
542
+ resolve({});
543
+ }
544
+ } catch (error) {
545
+ reject(new Error(`Invalid request body: ${error}`));
546
+ }
547
+ });
548
+ req.on("error", (error) => {
549
+ if (!rejected) reject(new Error(`Error reading request body: ${error.message}`));
550
+ });
551
+ });
552
+ }
553
+ /**
554
+ * CORS middleware
555
+ */
556
+ // Update the setCorsHeaders method in MikroServe class to handle allowed domains
557
+ setCorsHeaders(res, req) {
558
+ const origin = req.headers.origin;
559
+ const { allowedDomains = ["*"] } = this.config;
560
+ if (!origin || allowedDomains.length === 0) {
561
+ res.setHeader("Access-Control-Allow-Origin", "*");
562
+ } else if (allowedDomains.includes("*")) {
563
+ res.setHeader("Access-Control-Allow-Origin", "*");
564
+ } else if (allowedDomains.includes(origin)) {
565
+ res.setHeader("Access-Control-Allow-Origin", origin);
566
+ res.setHeader("Vary", "Origin");
567
+ }
568
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
569
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
570
+ res.setHeader("Access-Control-Max-Age", "86400");
571
+ }
572
+ /**
573
+ * Set security headers
574
+ */
575
+ setSecurityHeaders(res, isHttps = false) {
576
+ res.setHeader("X-Content-Type-Options", "nosniff");
577
+ res.setHeader("X-Frame-Options", "DENY");
578
+ res.setHeader(
579
+ "Content-Security-Policy",
580
+ "default-src 'self'; script-src 'self'; object-src 'none'"
581
+ );
582
+ if (isHttps) res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
583
+ res.setHeader("X-XSS-Protection", "1; mode=block");
584
+ }
585
+ /**
586
+ * Sends a response with appropriate headers.
587
+ * @param res - HTTP response object
588
+ * @param response - Response data and status code
589
+ */
590
+ respond(res, response) {
591
+ const headers = {
592
+ ...response.headers || {}
593
+ };
594
+ res.writeHead(response.statusCode, headers);
595
+ if (response.body === null || response.body === void 0) res.end();
596
+ else if (typeof response.body === "string") res.end(response.body);
597
+ else res.end(JSON.stringify(response.body));
598
+ }
599
+ /**
600
+ * Sets up graceful shutdown handlers for a server.
601
+ * @param server - The HTTP/HTTPS server to add shutdown handlers to
602
+ */
603
+ setupGracefulShutdown(server) {
604
+ const shutdown = (error) => {
605
+ console.log("Shutting down MikroServe server...");
606
+ if (error) console.error("Error:", error);
607
+ server.close(() => {
608
+ console.log("Server closed successfully");
609
+ setImmediate(() => {
610
+ process.exit(error ? 1 : 0);
611
+ });
612
+ });
613
+ };
614
+ process.on("SIGINT", () => shutdown());
615
+ process.on("SIGTERM", () => shutdown());
616
+ process.on("uncaughtException", shutdown);
617
+ process.on("unhandledRejection", shutdown);
618
+ }
619
+ };
620
+ // Annotate the CommonJS export names for ESM import in node:
621
+ 0 && (module.exports = {
622
+ MikroServe
623
+ });
package/lib/index.mjs ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ MikroServe
3
+ } from "./chunk-GGGGATKH.mjs";
4
+ import "./chunk-ZFBBESGU.mjs";
5
+ import "./chunk-KJT4SET2.mjs";
6
+ export {
7
+ MikroServe
8
+ };