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/LICENSE +7 -0
- package/README.md +3 -0
- package/lib/MikroServe.d.mts +106 -0
- package/lib/MikroServe.d.ts +106 -0
- package/lib/MikroServe.js +621 -0
- package/lib/MikroServe.mjs +8 -0
- package/lib/RateLimiter.d.mts +16 -0
- package/lib/RateLimiter.d.ts +16 -0
- package/lib/RateLimiter.js +73 -0
- package/lib/RateLimiter.mjs +6 -0
- package/lib/Router.d.mts +64 -0
- package/lib/Router.d.ts +64 -0
- package/lib/Router.js +224 -0
- package/lib/Router.mjs +6 -0
- package/lib/chunk-GGGGATKH.mjs +349 -0
- package/lib/chunk-KJT4SET2.mjs +200 -0
- package/lib/chunk-ZFBBESGU.mjs +49 -0
- package/lib/index.d.mts +4 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +623 -0
- package/lib/index.mjs +8 -0
- package/lib/interfaces/index.d.mts +264 -0
- package/lib/interfaces/index.d.ts +264 -0
- package/lib/interfaces/index.js +18 -0
- package/lib/interfaces/index.mjs +0 -0
- package/package.json +58 -0
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
|
+
});
|