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,349 @@
1
+ import {
2
+ RateLimiter
3
+ } from "./chunk-ZFBBESGU.mjs";
4
+ import {
5
+ Router
6
+ } from "./chunk-KJT4SET2.mjs";
7
+
8
+ // src/MikroServe.ts
9
+ import { readFileSync } from "node:fs";
10
+ import http from "node:http";
11
+ import https from "node:https";
12
+ var MikroServe = class {
13
+ config;
14
+ rateLimiter;
15
+ router;
16
+ useHttps;
17
+ sslCert;
18
+ sslKey;
19
+ sslCa;
20
+ debug;
21
+ /**
22
+ * @description Creates a new MikroServe instance.
23
+ * @param config - Server configuration options
24
+ */
25
+ constructor(config) {
26
+ this.config = config;
27
+ const { useHttps, sslCa, sslCert, sslKey, debug } = config;
28
+ const requestsPerMinute = config.rateLimit?.requestsPerMinute || 50;
29
+ this.rateLimiter = new RateLimiter(requestsPerMinute, 60);
30
+ this.router = new Router();
31
+ this.useHttps = useHttps || false;
32
+ this.sslCert = sslCert;
33
+ this.sslKey = sslKey;
34
+ this.sslCa = sslCa;
35
+ this.debug = debug || false;
36
+ if (config.rateLimit?.enabled !== false) this.use(this.rateLimitMiddleware.bind(this));
37
+ }
38
+ /**
39
+ * Register a global middleware
40
+ */
41
+ use(middleware) {
42
+ this.router.use(middleware);
43
+ return this;
44
+ }
45
+ /**
46
+ * Register a GET route
47
+ */
48
+ get(path, ...handlers) {
49
+ this.router.get(path, ...handlers);
50
+ return this;
51
+ }
52
+ /**
53
+ * Register a POST route
54
+ */
55
+ post(path, ...handlers) {
56
+ this.router.post(path, ...handlers);
57
+ return this;
58
+ }
59
+ /**
60
+ * Register a PUT route
61
+ */
62
+ put(path, ...handlers) {
63
+ this.router.put(path, ...handlers);
64
+ return this;
65
+ }
66
+ /**
67
+ * Register a DELETE route
68
+ */
69
+ delete(path, ...handlers) {
70
+ this.router.delete(path, ...handlers);
71
+ return this;
72
+ }
73
+ /**
74
+ * Register a PATCH route
75
+ */
76
+ patch(path, ...handlers) {
77
+ this.router.patch(path, ...handlers);
78
+ return this;
79
+ }
80
+ /**
81
+ * Register an OPTIONS route
82
+ */
83
+ options(path, ...handlers) {
84
+ this.router.options(path, ...handlers);
85
+ return this;
86
+ }
87
+ /**
88
+ * Creates an HTTP/HTTPS server, sets up graceful shutdown, and starts listening.
89
+ * @returns Running HTTP/HTTPS server
90
+ */
91
+ start() {
92
+ const server = this.createServer();
93
+ const { port = 3e3, host = "localhost" } = this.config;
94
+ this.setupGracefulShutdown(server);
95
+ server.listen(port, host, () => {
96
+ const address = server.address();
97
+ const protocol = this.useHttps ? "https" : "http";
98
+ console.log(
99
+ `MikroServe running at ${protocol}://${address.address !== "::" ? address.address : "localhost"}:${address.port}`
100
+ );
101
+ });
102
+ return server;
103
+ }
104
+ /**
105
+ * Creates and configures a server instance without starting it.
106
+ * @returns Configured HTTP or HTTPS server instance
107
+ */
108
+ createServer() {
109
+ const boundRequestHandler = this.requestHandler.bind(this);
110
+ if (this.useHttps) {
111
+ if (!this.sslCert || !this.sslKey)
112
+ throw new Error("SSL certificate and key paths are required when useHttps is true");
113
+ try {
114
+ const httpsOptions = {
115
+ key: readFileSync(this.sslKey),
116
+ cert: readFileSync(this.sslCert),
117
+ ...this.sslCa ? { ca: readFileSync(this.sslCa) } : {}
118
+ };
119
+ return https.createServer(httpsOptions, boundRequestHandler);
120
+ } catch (error) {
121
+ if (error.message.includes("key values mismatch"))
122
+ throw new Error(`SSL certificate and key do not match: ${error.message}`);
123
+ throw error;
124
+ }
125
+ }
126
+ return http.createServer(boundRequestHandler);
127
+ }
128
+ /**
129
+ * Rate limiting middleware
130
+ */
131
+ async rateLimitMiddleware(context, next) {
132
+ const ip = context.req.socket.remoteAddress || "unknown";
133
+ context.res.setHeader("X-RateLimit-Limit", this.rateLimiter.getLimit().toString());
134
+ context.res.setHeader(
135
+ "X-RateLimit-Remaining",
136
+ this.rateLimiter.getRemainingRequests(ip).toString()
137
+ );
138
+ context.res.setHeader("X-RateLimit-Reset", this.rateLimiter.getResetTime(ip).toString());
139
+ if (!this.rateLimiter.isAllowed(ip)) {
140
+ return {
141
+ statusCode: 429,
142
+ body: {
143
+ error: "Too Many Requests",
144
+ message: "Rate limit exceeded, please try again later"
145
+ },
146
+ headers: { "Content-Type": "application/json" }
147
+ };
148
+ }
149
+ return next();
150
+ }
151
+ /**
152
+ * Request handler for HTTP and HTTPS servers.
153
+ */
154
+ async requestHandler(req, res) {
155
+ const start = Date.now();
156
+ const method = req.method || "UNKNOWN";
157
+ const url = req.url || "/unknown";
158
+ try {
159
+ this.setCorsHeaders(res, req);
160
+ this.setSecurityHeaders(res, this.useHttps);
161
+ if (this.debug) console.log(`${method} ${url}`);
162
+ if (req.method === "OPTIONS") {
163
+ res.statusCode = 204;
164
+ res.end();
165
+ return;
166
+ }
167
+ try {
168
+ req.body = await this.parseBody(req);
169
+ } catch (error) {
170
+ if (this.debug) {
171
+ console.error("Body parsing error:", error.message);
172
+ }
173
+ return this.respond(res, {
174
+ statusCode: 400,
175
+ body: {
176
+ error: "Bad Request",
177
+ message: error.message
178
+ }
179
+ });
180
+ }
181
+ const result = await this.router.handle(req, res);
182
+ if (result) return this.respond(res, result);
183
+ return this.respond(res, {
184
+ statusCode: 404,
185
+ body: {
186
+ error: "Not Found",
187
+ message: "The requested endpoint does not exist"
188
+ }
189
+ });
190
+ } catch (error) {
191
+ console.error("Server error:", error);
192
+ return this.respond(res, {
193
+ statusCode: 500,
194
+ body: {
195
+ error: "Internal Server Error",
196
+ message: this.debug ? error.message : "An unexpected error occurred"
197
+ }
198
+ });
199
+ } finally {
200
+ if (this.debug) {
201
+ this.logDuration(start, method, url);
202
+ }
203
+ }
204
+ }
205
+ /**
206
+ * Writes out a clean log to represent the duration of the request.
207
+ */
208
+ logDuration(start, method, url) {
209
+ const duration = Date.now() - start;
210
+ console.log(`${method} ${url} completed in ${duration}ms`);
211
+ }
212
+ /**
213
+ * Parses the request body based on content type.
214
+ * @param req - HTTP request object
215
+ * @returns Promise resolving to the parsed body
216
+ * @throws Will throw if body cannot be parsed
217
+ */
218
+ /**
219
+ * Parses the request body based on content type.
220
+ * @param req - HTTP request object
221
+ * @returns Promise resolving to the parsed body
222
+ * @throws Will throw if body cannot be parsed
223
+ */
224
+ async parseBody(req) {
225
+ return new Promise((resolve, reject) => {
226
+ const bodyChunks = [];
227
+ let bodySize = 0;
228
+ const MAX_BODY_SIZE = 1024 * 1024;
229
+ let rejected = false;
230
+ const contentType = req.headers["content-type"] || "";
231
+ if (this.debug) {
232
+ console.log("Content-Type:", contentType);
233
+ }
234
+ req.on("data", (chunk) => {
235
+ bodySize += chunk.length;
236
+ if (this.debug)
237
+ console.log(`Received chunk: ${chunk.length} bytes, total size: ${bodySize}`);
238
+ if (bodySize > MAX_BODY_SIZE && !rejected) {
239
+ rejected = true;
240
+ if (this.debug) console.log(`Body size exceeded limit: ${bodySize} > ${MAX_BODY_SIZE}`);
241
+ reject(new Error("Request body too large"));
242
+ return;
243
+ }
244
+ if (!rejected) bodyChunks.push(chunk);
245
+ });
246
+ req.on("end", () => {
247
+ if (rejected) return;
248
+ if (this.debug) console.log(`Request body complete: ${bodySize} bytes`);
249
+ try {
250
+ if (bodyChunks.length > 0) {
251
+ const bodyString = Buffer.concat(bodyChunks).toString("utf8");
252
+ if (contentType.includes("application/json")) {
253
+ try {
254
+ resolve(JSON.parse(bodyString));
255
+ } catch (error) {
256
+ reject(new Error(`Invalid JSON in request body: ${error.message}`));
257
+ }
258
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
259
+ const formData = {};
260
+ new URLSearchParams(bodyString).forEach((value, key) => {
261
+ formData[key] = value;
262
+ });
263
+ resolve(formData);
264
+ } else {
265
+ resolve(bodyString);
266
+ }
267
+ } else {
268
+ resolve({});
269
+ }
270
+ } catch (error) {
271
+ reject(new Error(`Invalid request body: ${error}`));
272
+ }
273
+ });
274
+ req.on("error", (error) => {
275
+ if (!rejected) reject(new Error(`Error reading request body: ${error.message}`));
276
+ });
277
+ });
278
+ }
279
+ /**
280
+ * CORS middleware
281
+ */
282
+ // Update the setCorsHeaders method in MikroServe class to handle allowed domains
283
+ setCorsHeaders(res, req) {
284
+ const origin = req.headers.origin;
285
+ const { allowedDomains = ["*"] } = this.config;
286
+ if (!origin || allowedDomains.length === 0) {
287
+ res.setHeader("Access-Control-Allow-Origin", "*");
288
+ } else if (allowedDomains.includes("*")) {
289
+ res.setHeader("Access-Control-Allow-Origin", "*");
290
+ } else if (allowedDomains.includes(origin)) {
291
+ res.setHeader("Access-Control-Allow-Origin", origin);
292
+ res.setHeader("Vary", "Origin");
293
+ }
294
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
295
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
296
+ res.setHeader("Access-Control-Max-Age", "86400");
297
+ }
298
+ /**
299
+ * Set security headers
300
+ */
301
+ setSecurityHeaders(res, isHttps = false) {
302
+ res.setHeader("X-Content-Type-Options", "nosniff");
303
+ res.setHeader("X-Frame-Options", "DENY");
304
+ res.setHeader(
305
+ "Content-Security-Policy",
306
+ "default-src 'self'; script-src 'self'; object-src 'none'"
307
+ );
308
+ if (isHttps) res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
309
+ res.setHeader("X-XSS-Protection", "1; mode=block");
310
+ }
311
+ /**
312
+ * Sends a response with appropriate headers.
313
+ * @param res - HTTP response object
314
+ * @param response - Response data and status code
315
+ */
316
+ respond(res, response) {
317
+ const headers = {
318
+ ...response.headers || {}
319
+ };
320
+ res.writeHead(response.statusCode, headers);
321
+ if (response.body === null || response.body === void 0) res.end();
322
+ else if (typeof response.body === "string") res.end(response.body);
323
+ else res.end(JSON.stringify(response.body));
324
+ }
325
+ /**
326
+ * Sets up graceful shutdown handlers for a server.
327
+ * @param server - The HTTP/HTTPS server to add shutdown handlers to
328
+ */
329
+ setupGracefulShutdown(server) {
330
+ const shutdown = (error) => {
331
+ console.log("Shutting down MikroServe server...");
332
+ if (error) console.error("Error:", error);
333
+ server.close(() => {
334
+ console.log("Server closed successfully");
335
+ setImmediate(() => {
336
+ process.exit(error ? 1 : 0);
337
+ });
338
+ });
339
+ };
340
+ process.on("SIGINT", () => shutdown());
341
+ process.on("SIGTERM", () => shutdown());
342
+ process.on("uncaughtException", shutdown);
343
+ process.on("unhandledRejection", shutdown);
344
+ }
345
+ };
346
+
347
+ export {
348
+ MikroServe
349
+ };
@@ -0,0 +1,200 @@
1
+ // src/Router.ts
2
+ import { URL } from "node:url";
3
+ var Router = class {
4
+ routes = [];
5
+ globalMiddlewares = [];
6
+ pathPatterns = /* @__PURE__ */ new Map();
7
+ /**
8
+ * Add a global middleware
9
+ */
10
+ use(middleware) {
11
+ this.globalMiddlewares.push(middleware);
12
+ return this;
13
+ }
14
+ /**
15
+ * Register a route with specified method
16
+ */
17
+ register(method, path, handler, middlewares = []) {
18
+ this.routes.push({ method, path, handler, middlewares });
19
+ this.pathPatterns.set(path, this.createPathPattern(path));
20
+ return this;
21
+ }
22
+ /**
23
+ * Register a GET route
24
+ */
25
+ get(path, ...handlers) {
26
+ const handler = handlers.pop();
27
+ return this.register("GET", path, handler, handlers);
28
+ }
29
+ /**
30
+ * Register a POST route
31
+ */
32
+ post(path, ...handlers) {
33
+ const handler = handlers.pop();
34
+ return this.register("POST", path, handler, handlers);
35
+ }
36
+ /**
37
+ * Register a PUT route
38
+ */
39
+ put(path, ...handlers) {
40
+ const handler = handlers.pop();
41
+ return this.register("PUT", path, handler, handlers);
42
+ }
43
+ /**
44
+ * Register a DELETE route
45
+ */
46
+ delete(path, ...handlers) {
47
+ const handler = handlers.pop();
48
+ return this.register("DELETE", path, handler, handlers);
49
+ }
50
+ /**
51
+ * Register a PATCH route
52
+ */
53
+ patch(path, ...handlers) {
54
+ const handler = handlers.pop();
55
+ return this.register("PATCH", path, handler, handlers);
56
+ }
57
+ /**
58
+ * Register an OPTIONS route
59
+ */
60
+ options(path, ...handlers) {
61
+ const handler = handlers.pop();
62
+ return this.register("OPTIONS", path, handler, handlers);
63
+ }
64
+ /**
65
+ * Match a request to a route
66
+ */
67
+ match(method, path) {
68
+ for (const route of this.routes) {
69
+ if (route.method !== method) continue;
70
+ const pathPattern = this.pathPatterns.get(route.path);
71
+ if (!pathPattern) continue;
72
+ const match = pathPattern.pattern.exec(path);
73
+ if (!match) continue;
74
+ const params = {};
75
+ pathPattern.paramNames.forEach((name, index) => {
76
+ params[name] = match[index + 1] || "";
77
+ });
78
+ return { route, params };
79
+ }
80
+ return null;
81
+ }
82
+ /**
83
+ * Create a regex pattern for path matching
84
+ */
85
+ createPathPattern(path) {
86
+ const paramNames = [];
87
+ const pattern = path.replace(/\/:[^/]+/g, (match) => {
88
+ const paramName = match.slice(2);
89
+ paramNames.push(paramName);
90
+ return "/([^/]+)";
91
+ }).replace(/\/$/, "/?");
92
+ return {
93
+ pattern: new RegExp(`^${pattern}$`),
94
+ paramNames
95
+ };
96
+ }
97
+ /**
98
+ * Handle a request and find the matching route
99
+ */
100
+ async handle(req, res) {
101
+ const method = req.method || "GET";
102
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
103
+ const path = url.pathname;
104
+ const matched = this.match(method, path);
105
+ if (!matched) return null;
106
+ const { route, params } = matched;
107
+ const query = {};
108
+ url.searchParams.forEach((value, key) => {
109
+ query[key] = value;
110
+ });
111
+ const context = {
112
+ req,
113
+ res,
114
+ params,
115
+ query,
116
+ // @ts-ignore
117
+ body: req.body || {},
118
+ headers: req.headers,
119
+ path,
120
+ state: {},
121
+ // Add the missing state property
122
+ text: (content, status = 200) => ({
123
+ statusCode: status,
124
+ body: content,
125
+ headers: { "Content-Type": "text/plain" }
126
+ }),
127
+ form: (content, status = 200) => ({
128
+ statusCode: status,
129
+ body: content,
130
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
131
+ }),
132
+ json: (content, status = 200) => ({
133
+ statusCode: status,
134
+ body: content,
135
+ headers: { "Content-Type": "application/json" }
136
+ }),
137
+ html: (content, status = 200) => ({
138
+ statusCode: status,
139
+ body: content,
140
+ headers: { "Content-Type": "text/html" }
141
+ }),
142
+ redirect: (url2, status = 302) => ({
143
+ statusCode: status,
144
+ body: null,
145
+ headers: { Location: url2 }
146
+ }),
147
+ status: function(code) {
148
+ return {
149
+ text: (content) => ({
150
+ statusCode: code,
151
+ body: content,
152
+ headers: { "Content-Type": "text/plain" }
153
+ }),
154
+ json: (data) => ({
155
+ statusCode: code,
156
+ body: data,
157
+ headers: { "Content-Type": "application/json" }
158
+ }),
159
+ html: (content) => ({
160
+ statusCode: code,
161
+ body: content,
162
+ headers: { "Content-Type": "text/html" }
163
+ }),
164
+ form: (content) => ({
165
+ // Make sure form method is included here
166
+ statusCode: code,
167
+ body: content,
168
+ headers: { "Content-Type": "application/x-www-form-urlencoded" }
169
+ }),
170
+ redirect: (url2, redirectStatus = 302) => ({
171
+ statusCode: redirectStatus,
172
+ body: null,
173
+ headers: { Location: url2 }
174
+ }),
175
+ status: (updatedCode) => this.status(updatedCode)
176
+ };
177
+ }
178
+ };
179
+ const middlewares = [...this.globalMiddlewares, ...route.middlewares];
180
+ return this.executeMiddlewareChain(context, middlewares, route.handler);
181
+ }
182
+ /**
183
+ * Execute middleware chain and final handler
184
+ */
185
+ async executeMiddlewareChain(context, middlewares, finalHandler) {
186
+ let currentIndex = 0;
187
+ const next = async () => {
188
+ if (currentIndex < middlewares.length) {
189
+ const middleware = middlewares[currentIndex++];
190
+ return middleware(context, next);
191
+ }
192
+ return finalHandler(context);
193
+ };
194
+ return next();
195
+ }
196
+ };
197
+
198
+ export {
199
+ Router
200
+ };
@@ -0,0 +1,49 @@
1
+ // src/RateLimiter.ts
2
+ var RateLimiter = class {
3
+ requests = /* @__PURE__ */ new Map();
4
+ limit;
5
+ windowMs;
6
+ constructor(limit = 100, windowSeconds = 60) {
7
+ this.limit = limit;
8
+ this.windowMs = windowSeconds * 1e3;
9
+ setInterval(() => this.cleanup(), this.windowMs);
10
+ }
11
+ getLimit() {
12
+ return this.limit;
13
+ }
14
+ isAllowed(ip) {
15
+ const now = Date.now();
16
+ const key = ip || "unknown";
17
+ let entry = this.requests.get(key);
18
+ if (!entry || entry.resetTime < now) {
19
+ entry = { count: 0, resetTime: now + this.windowMs };
20
+ this.requests.set(key, entry);
21
+ }
22
+ entry.count++;
23
+ return entry.count <= this.limit;
24
+ }
25
+ getRemainingRequests(ip) {
26
+ const now = Date.now();
27
+ const key = ip || "unknown";
28
+ const entry = this.requests.get(key);
29
+ if (!entry || entry.resetTime < now) return this.limit;
30
+ return Math.max(0, this.limit - entry.count);
31
+ }
32
+ getResetTime(ip) {
33
+ const now = Date.now();
34
+ const key = ip || "unknown";
35
+ const entry = this.requests.get(key);
36
+ if (!entry || entry.resetTime < now) return Math.floor((now + this.windowMs) / 1e3);
37
+ return Math.floor(entry.resetTime / 1e3);
38
+ }
39
+ cleanup() {
40
+ const now = Date.now();
41
+ for (const [key, entry] of this.requests.entries()) {
42
+ if (entry.resetTime < now) this.requests.delete(key);
43
+ }
44
+ }
45
+ };
46
+
47
+ export {
48
+ RateLimiter
49
+ };
@@ -0,0 +1,4 @@
1
+ export { MikroServe } from './MikroServe.mjs';
2
+ import 'node:http';
3
+ import 'node:https';
4
+ import './interfaces/index.mjs';
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { MikroServe } from './MikroServe.js';
2
+ import 'node:http';
3
+ import 'node:https';
4
+ import './interfaces/index.js';