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