@abejarano/ts-express-server 1.5.3 → 1.7.0
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 +14 -2
- package/dist/BootstrapServer.d.ts +13 -6
- package/dist/BootstrapServer.js +30 -23
- package/dist/BootstrapStandardServer.d.ts +2 -1
- package/dist/BootstrapStandardServer.js +6 -4
- package/dist/abstract/ServerModule.d.ts +2 -2
- package/dist/abstract/ServerService.d.ts +2 -2
- package/dist/abstract/ServerTypes.d.ts +64 -0
- package/dist/abstract/ServerTypes.js +8 -0
- package/dist/abstract/index.d.ts +1 -0
- package/dist/abstract/index.js +1 -0
- package/dist/adapters/BunAdapter.d.ts +18 -0
- package/dist/adapters/BunAdapter.js +741 -0
- package/dist/adapters/ExpressAdapter.d.ts +8 -0
- package/dist/adapters/ExpressAdapter.js +40 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +18 -0
- package/dist/createRouter.d.ts +2 -0
- package/dist/createRouter.js +12 -0
- package/dist/decorators/Controller.d.ts +1 -1
- package/dist/decorators/Controller.js +4 -1
- package/dist/decorators/DecoratorGuards.d.ts +1 -0
- package/dist/decorators/DecoratorGuards.js +14 -0
- package/dist/decorators/Handlers.d.ts +5 -5
- package/dist/decorators/Handlers.js +5 -1
- package/dist/decorators/Use.d.ts +2 -2
- package/dist/decorators/Use.js +5 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/modules/ControllersModule.d.ts +2 -2
- package/dist/modules/ControllersModule.js +14 -8
- package/dist/modules/CorsModule.d.ts +2 -2
- package/dist/modules/CorsModule.js +66 -2
- package/dist/modules/FileUploadModule.d.ts +4 -4
- package/dist/modules/FileUploadModule.js +43 -6
- package/dist/modules/RateLimitModule.d.ts +4 -4
- package/dist/modules/RateLimitModule.js +48 -7
- package/dist/modules/RequestContextModule.d.ts +2 -9
- package/dist/modules/RequestContextModule.js +53 -4
- package/dist/modules/RoutesModule.d.ts +4 -4
- package/dist/modules/RoutesModule.js +12 -3
- package/dist/modules/SecurityModule.d.ts +4 -4
- package/dist/modules/SecurityModule.js +43 -6
- package/dist/testing/createDecoratedTestApp.d.ts +3 -3
- package/dist/testing/createDecoratedTestApp.js +8 -4
- package/package.json +1 -1
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BunAdapter = void 0;
|
|
4
|
+
exports.getFiles = getFiles;
|
|
5
|
+
exports.getFile = getFile;
|
|
6
|
+
const ServerTypes_1 = require("../abstract/ServerTypes");
|
|
7
|
+
class BunResponse {
|
|
8
|
+
constructor(cookieJar, handlerTimeoutMs) {
|
|
9
|
+
this.statusCode = 200;
|
|
10
|
+
this.statusExplicitlySet = false;
|
|
11
|
+
this.headers = new Headers();
|
|
12
|
+
this.setCookies = [];
|
|
13
|
+
this.body = null;
|
|
14
|
+
this.ended = false;
|
|
15
|
+
this.cookieJar = cookieJar;
|
|
16
|
+
this.handlerTimeoutMs = handlerTimeoutMs;
|
|
17
|
+
this.endPromise = new Promise((resolve) => {
|
|
18
|
+
this.resolveEnd = resolve;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
status(code) {
|
|
22
|
+
this.statusExplicitlySet = true;
|
|
23
|
+
this.statusCode = code;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
set(name, value) {
|
|
27
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
28
|
+
this.setCookies.push(value);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
this.headers.set(name, value);
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
header(name, value) {
|
|
35
|
+
return this.set(name, value);
|
|
36
|
+
}
|
|
37
|
+
cookie(name, value, options = {}) {
|
|
38
|
+
if (this.cookieJar && typeof this.cookieJar.set === "function") {
|
|
39
|
+
this.cookieJar.set(name, value, toCookieJarOptions(options));
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
this.setCookies.push(serializeCookie(name, value, options));
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
json(body) {
|
|
46
|
+
if (!this.headers.has("content-type")) {
|
|
47
|
+
this.headers.set("content-type", "application/json");
|
|
48
|
+
}
|
|
49
|
+
this.body = JSON.stringify(body ?? null);
|
|
50
|
+
this.ended = true;
|
|
51
|
+
this.resolveEnd?.();
|
|
52
|
+
}
|
|
53
|
+
send(body) {
|
|
54
|
+
if (body instanceof Response) {
|
|
55
|
+
this.rawResponse = body;
|
|
56
|
+
this.ended = true;
|
|
57
|
+
this.resolveEnd?.();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
61
|
+
this.body = body;
|
|
62
|
+
this.ended = true;
|
|
63
|
+
this.resolveEnd?.();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (body === undefined) {
|
|
67
|
+
this.body = null;
|
|
68
|
+
this.ended = true;
|
|
69
|
+
this.resolveEnd?.();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!this.headers.has("content-type")) {
|
|
73
|
+
this.headers.set("content-type", "application/json");
|
|
74
|
+
}
|
|
75
|
+
this.body = JSON.stringify(body);
|
|
76
|
+
this.ended = true;
|
|
77
|
+
this.resolveEnd?.();
|
|
78
|
+
}
|
|
79
|
+
end(body) {
|
|
80
|
+
if (body !== undefined) {
|
|
81
|
+
this.send(body);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.ended = true;
|
|
85
|
+
this.resolveEnd?.();
|
|
86
|
+
}
|
|
87
|
+
isEnded() {
|
|
88
|
+
return this.ended;
|
|
89
|
+
}
|
|
90
|
+
waitForEnd() {
|
|
91
|
+
return this.endPromise;
|
|
92
|
+
}
|
|
93
|
+
getHandlerTimeoutMs() {
|
|
94
|
+
return this.handlerTimeoutMs;
|
|
95
|
+
}
|
|
96
|
+
toResponse() {
|
|
97
|
+
if (this.rawResponse) {
|
|
98
|
+
const headerMap = new Map();
|
|
99
|
+
const setCookies = readSetCookieHeaders(this.rawResponse.headers);
|
|
100
|
+
this.rawResponse.headers.forEach((value, key) => {
|
|
101
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
headerMap.set(key.toLowerCase(), value);
|
|
105
|
+
});
|
|
106
|
+
this.headers.forEach((value, key) => {
|
|
107
|
+
headerMap.set(key.toLowerCase(), value);
|
|
108
|
+
});
|
|
109
|
+
setCookies.push(...this.setCookies);
|
|
110
|
+
const headersInit = [];
|
|
111
|
+
headerMap.forEach((value, key) => {
|
|
112
|
+
headersInit.push([key, value]);
|
|
113
|
+
});
|
|
114
|
+
for (const cookie of setCookies) {
|
|
115
|
+
headersInit.push(["set-cookie", cookie]);
|
|
116
|
+
}
|
|
117
|
+
return new Response(this.rawResponse.body, {
|
|
118
|
+
status: this.statusExplicitlySet
|
|
119
|
+
? this.statusCode
|
|
120
|
+
: this.rawResponse.status,
|
|
121
|
+
headers: headersInit,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const headersInit = [];
|
|
125
|
+
this.headers.forEach((value, key) => {
|
|
126
|
+
headersInit.push([key, value]);
|
|
127
|
+
});
|
|
128
|
+
for (const cookie of this.setCookies) {
|
|
129
|
+
headersInit.push(["set-cookie", cookie]);
|
|
130
|
+
}
|
|
131
|
+
return new Response(this.body, {
|
|
132
|
+
status: this.statusCode,
|
|
133
|
+
headers: headersInit,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
class BunRouter {
|
|
138
|
+
constructor() {
|
|
139
|
+
this.middlewares = [];
|
|
140
|
+
this.routes = [];
|
|
141
|
+
}
|
|
142
|
+
use(pathOrHandler, ...handlers) {
|
|
143
|
+
if (typeof pathOrHandler === "string") {
|
|
144
|
+
const path = pathOrHandler;
|
|
145
|
+
const resolvedHandlers = normalizeHandlers(handlers).map((handler) => handler instanceof BunRouter
|
|
146
|
+
? this.wrapRouter(handler, path)
|
|
147
|
+
: handler);
|
|
148
|
+
this.middlewares.push({ path, handlers: resolvedHandlers });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const resolvedHandlers = normalizeHandlers([pathOrHandler, ...handlers]);
|
|
152
|
+
const wrappedHandlers = resolvedHandlers.map((handler) => handler instanceof BunRouter
|
|
153
|
+
? this.wrapRouter(handler)
|
|
154
|
+
: handler);
|
|
155
|
+
this.middlewares.push({ handlers: wrappedHandlers });
|
|
156
|
+
}
|
|
157
|
+
get(path, ...handlers) {
|
|
158
|
+
this.routes.push({ method: "GET", path, handlers });
|
|
159
|
+
}
|
|
160
|
+
post(path, ...handlers) {
|
|
161
|
+
this.routes.push({ method: "POST", path, handlers });
|
|
162
|
+
}
|
|
163
|
+
put(path, ...handlers) {
|
|
164
|
+
this.routes.push({ method: "PUT", path, handlers });
|
|
165
|
+
}
|
|
166
|
+
delete(path, ...handlers) {
|
|
167
|
+
this.routes.push({ method: "DELETE", path, handlers });
|
|
168
|
+
}
|
|
169
|
+
patch(path, ...handlers) {
|
|
170
|
+
this.routes.push({ method: "PATCH", path, handlers });
|
|
171
|
+
}
|
|
172
|
+
async handle(req, res, done, pathOverride, suppressNotFound) {
|
|
173
|
+
const path = normalizePath(pathOverride ?? req.path);
|
|
174
|
+
const middlewares = this.collectMiddlewares(path);
|
|
175
|
+
const routeMatch = this.matchRoute(req.method, path);
|
|
176
|
+
if (routeMatch) {
|
|
177
|
+
req.params = routeMatch.params;
|
|
178
|
+
}
|
|
179
|
+
const handlers = [
|
|
180
|
+
...middlewares.flatMap((mw) => mw.handlers),
|
|
181
|
+
...(routeMatch ? routeMatch.route.handlers : []),
|
|
182
|
+
];
|
|
183
|
+
let chainCompleted = false;
|
|
184
|
+
let timeoutId;
|
|
185
|
+
let timedOut = false;
|
|
186
|
+
const timeoutMs = res.getHandlerTimeoutMs();
|
|
187
|
+
const timeoutPromise = typeof timeoutMs === "number" && timeoutMs > 0
|
|
188
|
+
? new Promise((resolve) => {
|
|
189
|
+
timeoutId = setTimeout(() => {
|
|
190
|
+
timedOut = true;
|
|
191
|
+
if (!res.isEnded()) {
|
|
192
|
+
res.status(504).json({ message: "Handler timeout" });
|
|
193
|
+
}
|
|
194
|
+
resolve(false);
|
|
195
|
+
}, timeoutMs);
|
|
196
|
+
})
|
|
197
|
+
: null;
|
|
198
|
+
try {
|
|
199
|
+
chainCompleted = timeoutPromise
|
|
200
|
+
? await Promise.race([runHandlers(handlers, req, res), timeoutPromise])
|
|
201
|
+
: await runHandlers(handlers, req, res);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!res.isEnded()) {
|
|
205
|
+
res.status(500).json({ message: "Internal server error" });
|
|
206
|
+
}
|
|
207
|
+
done(error);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
if (timeoutId) {
|
|
212
|
+
clearTimeout(timeoutId);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (timedOut) {
|
|
216
|
+
done();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!routeMatch) {
|
|
220
|
+
if (chainCompleted && !res.isEnded() && !suppressNotFound) {
|
|
221
|
+
res.status(404).json({ message: "Not found" });
|
|
222
|
+
}
|
|
223
|
+
if (chainCompleted) {
|
|
224
|
+
done();
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (chainCompleted) {
|
|
229
|
+
done();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
collectMiddlewares(path) {
|
|
233
|
+
return this.middlewares.filter((layer) => {
|
|
234
|
+
if (!layer.path) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
const normalized = normalizePath(layer.path);
|
|
238
|
+
if (normalized === "/") {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return path === normalized || path.startsWith(`${normalized}/`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
matchRoute(method, path) {
|
|
245
|
+
for (const route of this.routes) {
|
|
246
|
+
if (route.method !== method.toUpperCase()) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const match = matchPath(route.path, path);
|
|
250
|
+
if (match) {
|
|
251
|
+
return { route, params: match };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
wrapRouter(router, basePath) {
|
|
257
|
+
return async (req, res, next) => {
|
|
258
|
+
if (!basePath) {
|
|
259
|
+
await router.handle(req, res, next, undefined, true);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const normalizedBase = normalizePath(basePath);
|
|
263
|
+
const currentPath = normalizePath(req.path);
|
|
264
|
+
if (normalizedBase === "/") {
|
|
265
|
+
await router.handle(req, res, next, currentPath, true);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (currentPath !== normalizedBase &&
|
|
269
|
+
!currentPath.startsWith(`${normalizedBase}/`)) {
|
|
270
|
+
return next();
|
|
271
|
+
}
|
|
272
|
+
const nestedPath = stripPrefix(currentPath, normalizedBase);
|
|
273
|
+
await router.handle(req, res, next, nestedPath, true);
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
class BunApp extends BunRouter {
|
|
278
|
+
constructor() {
|
|
279
|
+
super(...arguments);
|
|
280
|
+
this.settings = new Map();
|
|
281
|
+
}
|
|
282
|
+
set(key, value) {
|
|
283
|
+
this.settings.set(key, value);
|
|
284
|
+
}
|
|
285
|
+
get(key) {
|
|
286
|
+
return this.settings.get(key);
|
|
287
|
+
}
|
|
288
|
+
createFetchHandler() {
|
|
289
|
+
return async (request, server) => {
|
|
290
|
+
const client = server?.requestIP?.(request);
|
|
291
|
+
const cookieJar = request.cookies;
|
|
292
|
+
const handlerTimeoutMs = this.get("handlerTimeoutMs");
|
|
293
|
+
const trustProxy = this.get("trustProxy") === true;
|
|
294
|
+
const ipOverride = trustProxy ? undefined : client?.address;
|
|
295
|
+
const req = createRequest(request, ipOverride);
|
|
296
|
+
const res = new BunResponse(cookieJar, typeof handlerTimeoutMs === "number" ? handlerTimeoutMs : undefined);
|
|
297
|
+
await this.handle(req, res, () => undefined);
|
|
298
|
+
if (!res.isEnded()) {
|
|
299
|
+
res.status(204).end();
|
|
300
|
+
}
|
|
301
|
+
return res.toResponse();
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
class BunAdapter {
|
|
306
|
+
constructor() {
|
|
307
|
+
this.runtime = ServerTypes_1.ServerRuntime.Bun;
|
|
308
|
+
}
|
|
309
|
+
createApp() {
|
|
310
|
+
return new BunApp();
|
|
311
|
+
}
|
|
312
|
+
createRouter() {
|
|
313
|
+
return new BunRouter();
|
|
314
|
+
}
|
|
315
|
+
configure(app, _port) {
|
|
316
|
+
const bunApp = app;
|
|
317
|
+
bunApp.use(createMultipartBodyParser(bunApp));
|
|
318
|
+
bunApp.use(createJsonBodyParser(bunApp));
|
|
319
|
+
bunApp.use(createUrlEncodedBodyParser(bunApp));
|
|
320
|
+
}
|
|
321
|
+
listen(app, port, onListen) {
|
|
322
|
+
const bunApp = app;
|
|
323
|
+
const server = Bun.serve({
|
|
324
|
+
port,
|
|
325
|
+
fetch: bunApp.createFetchHandler(),
|
|
326
|
+
});
|
|
327
|
+
onListen();
|
|
328
|
+
return {
|
|
329
|
+
close: (callback) => {
|
|
330
|
+
server.stop(true);
|
|
331
|
+
callback?.();
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
exports.BunAdapter = BunAdapter;
|
|
337
|
+
const createJsonBodyParser = (app) => {
|
|
338
|
+
return async (req, res, next) => {
|
|
339
|
+
if (!req.raw || req.body !== undefined) {
|
|
340
|
+
return next();
|
|
341
|
+
}
|
|
342
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
343
|
+
if (!contentType.includes("application/json")) {
|
|
344
|
+
return next();
|
|
345
|
+
}
|
|
346
|
+
const limit = getBodyLimit(app);
|
|
347
|
+
const contentLength = parseContentLength(req.headers["content-length"]);
|
|
348
|
+
if (contentLength !== undefined && contentLength > limit) {
|
|
349
|
+
res.status(413).json({ message: "Payload too large" });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
req.body = await req.raw.json();
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
res.status(400).json({ message: "Invalid JSON" });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
next();
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const createUrlEncodedBodyParser = (app) => {
|
|
363
|
+
return async (req, res, next) => {
|
|
364
|
+
if (!req.raw || req.body !== undefined) {
|
|
365
|
+
return next();
|
|
366
|
+
}
|
|
367
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
368
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
369
|
+
return next();
|
|
370
|
+
}
|
|
371
|
+
const limit = getBodyLimit(app);
|
|
372
|
+
const contentLength = parseContentLength(req.headers["content-length"]);
|
|
373
|
+
if (contentLength !== undefined && contentLength > limit) {
|
|
374
|
+
res.status(413).json({ message: "Payload too large" });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const text = await req.raw.text();
|
|
378
|
+
req.body = Object.fromEntries(new URLSearchParams(text));
|
|
379
|
+
next();
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
const createMultipartBodyParser = (app) => {
|
|
383
|
+
return async (req, res, next) => {
|
|
384
|
+
if (!req.raw || req.body !== undefined) {
|
|
385
|
+
return next();
|
|
386
|
+
}
|
|
387
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
388
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
389
|
+
return next();
|
|
390
|
+
}
|
|
391
|
+
const options = normalizeMultipartOptions(app.get("multipart"));
|
|
392
|
+
const lengthHeader = req.headers["content-length"];
|
|
393
|
+
const contentLength = parseContentLength(lengthHeader);
|
|
394
|
+
if (contentLength !== undefined && contentLength > options.maxBodyBytes) {
|
|
395
|
+
res.status(413).json({ message: "Payload too large" });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const formData = await req.raw.formData();
|
|
400
|
+
const fields = {};
|
|
401
|
+
const files = {};
|
|
402
|
+
let fileCount = 0;
|
|
403
|
+
for (const [key, value] of formData.entries()) {
|
|
404
|
+
if (isFile(value)) {
|
|
405
|
+
if (value.size > options.maxFileBytes) {
|
|
406
|
+
res.status(413).json({ message: "Payload too large" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!isMimeAllowed(value.type, options.allowedMimeTypes)) {
|
|
410
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
fileCount += 1;
|
|
414
|
+
if (fileCount > options.maxFiles) {
|
|
415
|
+
res.status(413).json({ message: "Payload too large" });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const bucket = files[key];
|
|
419
|
+
if (bucket) {
|
|
420
|
+
bucket.push(value);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
files[key] = [value];
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const existing = fields[key];
|
|
428
|
+
const textValue = String(value);
|
|
429
|
+
if (existing === undefined) {
|
|
430
|
+
fields[key] = textValue;
|
|
431
|
+
}
|
|
432
|
+
else if (Array.isArray(existing)) {
|
|
433
|
+
existing.push(textValue);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
fields[key] = [existing, textValue];
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (Object.keys(fields).length > 0) {
|
|
440
|
+
req.body = fields;
|
|
441
|
+
}
|
|
442
|
+
if (Object.keys(files).length > 0) {
|
|
443
|
+
req.files = files;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
res.status(400).json({ message: "Invalid multipart form data" });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
next();
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
function createRequest(request, ipOverride) {
|
|
454
|
+
const url = new URL(request.url);
|
|
455
|
+
const headers = toHeaderRecord(request.headers);
|
|
456
|
+
const query = toQueryRecord(url.searchParams);
|
|
457
|
+
return {
|
|
458
|
+
method: request.method.toUpperCase(),
|
|
459
|
+
path: normalizePath(url.pathname),
|
|
460
|
+
originalUrl: url.pathname + url.search,
|
|
461
|
+
params: {},
|
|
462
|
+
query,
|
|
463
|
+
headers,
|
|
464
|
+
cookies: parseCookies(headers.cookie),
|
|
465
|
+
ip: ipOverride || extractIp(headers),
|
|
466
|
+
raw: request,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function toHeaderRecord(headers) {
|
|
470
|
+
const record = {};
|
|
471
|
+
headers.forEach((value, key) => {
|
|
472
|
+
record[key.toLowerCase()] = value;
|
|
473
|
+
});
|
|
474
|
+
return record;
|
|
475
|
+
}
|
|
476
|
+
function toQueryRecord(search) {
|
|
477
|
+
const record = {};
|
|
478
|
+
for (const [key, value] of search.entries()) {
|
|
479
|
+
const existing = record[key];
|
|
480
|
+
if (existing === undefined) {
|
|
481
|
+
record[key] = value;
|
|
482
|
+
}
|
|
483
|
+
else if (Array.isArray(existing)) {
|
|
484
|
+
existing.push(value);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
record[key] = [existing, value];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return record;
|
|
491
|
+
}
|
|
492
|
+
const DEFAULT_MULTIPART_OPTIONS = {
|
|
493
|
+
maxBodyBytes: 10 * 1024 * 1024,
|
|
494
|
+
maxFileBytes: 10 * 1024 * 1024,
|
|
495
|
+
maxFiles: 10,
|
|
496
|
+
};
|
|
497
|
+
function serializeCookie(name, value, options) {
|
|
498
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
499
|
+
if (options.maxAge !== undefined) {
|
|
500
|
+
const maxAgeSeconds = Math.floor(options.maxAge / 1000);
|
|
501
|
+
parts.push(`Max-Age=${maxAgeSeconds}`);
|
|
502
|
+
if (!options.expires) {
|
|
503
|
+
parts.push(`Expires=${new Date(Date.now() + options.maxAge).toUTCString()}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (options.domain) {
|
|
507
|
+
parts.push(`Domain=${options.domain}`);
|
|
508
|
+
}
|
|
509
|
+
if (options.path) {
|
|
510
|
+
parts.push(`Path=${options.path}`);
|
|
511
|
+
}
|
|
512
|
+
if (options.expires) {
|
|
513
|
+
parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
514
|
+
}
|
|
515
|
+
if (options.httpOnly) {
|
|
516
|
+
parts.push("HttpOnly");
|
|
517
|
+
}
|
|
518
|
+
if (options.secure || options.sameSite === "none") {
|
|
519
|
+
parts.push("Secure");
|
|
520
|
+
}
|
|
521
|
+
if (options.sameSite) {
|
|
522
|
+
const normalized = options.sameSite === "none"
|
|
523
|
+
? "None"
|
|
524
|
+
: options.sameSite === "strict"
|
|
525
|
+
? "Strict"
|
|
526
|
+
: "Lax";
|
|
527
|
+
parts.push(`SameSite=${normalized}`);
|
|
528
|
+
}
|
|
529
|
+
return parts.join("; ");
|
|
530
|
+
}
|
|
531
|
+
function toCookieJarOptions(options) {
|
|
532
|
+
const sameSite = options.sameSite === "none"
|
|
533
|
+
? "None"
|
|
534
|
+
: options.sameSite === "strict"
|
|
535
|
+
? "Strict"
|
|
536
|
+
: options.sameSite === "lax"
|
|
537
|
+
? "Lax"
|
|
538
|
+
: undefined;
|
|
539
|
+
const maxAge = options.maxAge === undefined
|
|
540
|
+
? undefined
|
|
541
|
+
: Math.floor(options.maxAge / 1000);
|
|
542
|
+
return {
|
|
543
|
+
maxAge,
|
|
544
|
+
domain: options.domain,
|
|
545
|
+
path: options.path,
|
|
546
|
+
expires: options.expires,
|
|
547
|
+
httpOnly: options.httpOnly,
|
|
548
|
+
secure: options.secure || options.sameSite === "none",
|
|
549
|
+
sameSite,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function getBodyLimit(app) {
|
|
553
|
+
return normalizeMultipartOptions(app.get("multipart")).maxBodyBytes;
|
|
554
|
+
}
|
|
555
|
+
function normalizeMultipartOptions(input) {
|
|
556
|
+
if (!input || typeof input !== "object") {
|
|
557
|
+
return { ...DEFAULT_MULTIPART_OPTIONS };
|
|
558
|
+
}
|
|
559
|
+
const value = input;
|
|
560
|
+
return {
|
|
561
|
+
maxBodyBytes: value.maxBodyBytes ?? DEFAULT_MULTIPART_OPTIONS.maxBodyBytes,
|
|
562
|
+
maxFileBytes: value.maxFileBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFileBytes,
|
|
563
|
+
maxFiles: value.maxFiles ?? DEFAULT_MULTIPART_OPTIONS.maxFiles,
|
|
564
|
+
allowedMimeTypes: value.allowedMimeTypes,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function isMimeAllowed(type, allowed) {
|
|
568
|
+
if (!allowed || allowed.length === 0) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
const normalized = type.toLowerCase();
|
|
572
|
+
for (const entry of allowed) {
|
|
573
|
+
const rule = entry.toLowerCase();
|
|
574
|
+
if (rule.endsWith("/*")) {
|
|
575
|
+
const prefix = rule.slice(0, -1);
|
|
576
|
+
if (normalized.startsWith(prefix)) {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (normalized === rule) {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
function isFile(value) {
|
|
588
|
+
if (!value || typeof value !== "object") {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
return (typeof value.arrayBuffer === "function" &&
|
|
592
|
+
typeof value.name === "string" &&
|
|
593
|
+
typeof value.size === "number");
|
|
594
|
+
}
|
|
595
|
+
function parseCookies(cookieHeader) {
|
|
596
|
+
if (!cookieHeader) {
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
const cookies = {};
|
|
600
|
+
cookieHeader.split(";").forEach((entry) => {
|
|
601
|
+
const [name, ...rest] = entry.trim().split("=");
|
|
602
|
+
if (!name) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
cookies[name] = decodeURIComponent(rest.join("="));
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
cookies[name] = rest.join("=");
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
return cookies;
|
|
613
|
+
}
|
|
614
|
+
function parseContentLength(header) {
|
|
615
|
+
if (header === undefined) {
|
|
616
|
+
return undefined;
|
|
617
|
+
}
|
|
618
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
619
|
+
if (!value) {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
const parsed = Number.parseInt(value, 10);
|
|
623
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
624
|
+
}
|
|
625
|
+
function readSetCookieHeaders(headers) {
|
|
626
|
+
const bunHeaders = headers;
|
|
627
|
+
const setCookieFromApi = bunHeaders.getSetCookie?.() ?? bunHeaders.getAll?.("Set-Cookie");
|
|
628
|
+
if (setCookieFromApi && setCookieFromApi.length > 0) {
|
|
629
|
+
return setCookieFromApi;
|
|
630
|
+
}
|
|
631
|
+
const setCookies = [];
|
|
632
|
+
headers.forEach((value, key) => {
|
|
633
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
634
|
+
setCookies.push(value);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
return setCookies;
|
|
638
|
+
}
|
|
639
|
+
function getFiles(req, field) {
|
|
640
|
+
const map = (req.files ?? {});
|
|
641
|
+
return map[field] ?? [];
|
|
642
|
+
}
|
|
643
|
+
function getFile(req, field) {
|
|
644
|
+
return getFiles(req, field)[0];
|
|
645
|
+
}
|
|
646
|
+
function extractIp(headers) {
|
|
647
|
+
const forwarded = headers["x-forwarded-for"];
|
|
648
|
+
if (forwarded) {
|
|
649
|
+
return forwarded.split(",")[0]?.trim();
|
|
650
|
+
}
|
|
651
|
+
return headers["x-real-ip"];
|
|
652
|
+
}
|
|
653
|
+
function normalizePath(path) {
|
|
654
|
+
if (!path.startsWith("/")) {
|
|
655
|
+
path = `/${path}`;
|
|
656
|
+
}
|
|
657
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
658
|
+
return path.slice(0, -1);
|
|
659
|
+
}
|
|
660
|
+
return path;
|
|
661
|
+
}
|
|
662
|
+
function stripPrefix(path, prefix) {
|
|
663
|
+
if (!path.startsWith(prefix)) {
|
|
664
|
+
return path;
|
|
665
|
+
}
|
|
666
|
+
const stripped = path.slice(prefix.length);
|
|
667
|
+
return normalizePath(stripped || "/");
|
|
668
|
+
}
|
|
669
|
+
function matchPath(routePath, requestPath) {
|
|
670
|
+
const normalizedRoute = normalizePath(routePath);
|
|
671
|
+
const normalizedRequest = normalizePath(requestPath);
|
|
672
|
+
if (normalizedRoute === normalizedRequest) {
|
|
673
|
+
return {};
|
|
674
|
+
}
|
|
675
|
+
const routeParts = normalizedRoute.split("/").filter(Boolean);
|
|
676
|
+
const requestParts = normalizedRequest.split("/").filter(Boolean);
|
|
677
|
+
if (routeParts.length !== requestParts.length) {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
const params = {};
|
|
681
|
+
for (let index = 0; index < routeParts.length; index += 1) {
|
|
682
|
+
const routePart = routeParts[index];
|
|
683
|
+
const requestPart = requestParts[index];
|
|
684
|
+
if (routePart.startsWith(":")) {
|
|
685
|
+
params[routePart.slice(1)] = decodeURIComponent(requestPart);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (routePart !== requestPart) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return params;
|
|
693
|
+
}
|
|
694
|
+
function normalizeHandlers(inputs) {
|
|
695
|
+
const handlers = [];
|
|
696
|
+
for (const input of inputs) {
|
|
697
|
+
if (Array.isArray(input)) {
|
|
698
|
+
handlers.push(...input);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
handlers.push(input);
|
|
702
|
+
}
|
|
703
|
+
return handlers;
|
|
704
|
+
}
|
|
705
|
+
async function runHandlers(handlers, req, res) {
|
|
706
|
+
let index = 0;
|
|
707
|
+
const dispatch = async () => {
|
|
708
|
+
const handler = handlers[index];
|
|
709
|
+
index += 1;
|
|
710
|
+
if (!handler) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
let nextCalled = false;
|
|
714
|
+
let nextPromise;
|
|
715
|
+
let resolveNext;
|
|
716
|
+
const nextSignal = new Promise((resolve) => {
|
|
717
|
+
resolveNext = resolve;
|
|
718
|
+
});
|
|
719
|
+
const wrappedNext = (nextErr) => {
|
|
720
|
+
if (nextErr) {
|
|
721
|
+
throw nextErr;
|
|
722
|
+
}
|
|
723
|
+
nextCalled = true;
|
|
724
|
+
resolveNext?.();
|
|
725
|
+
nextPromise = dispatch();
|
|
726
|
+
return nextPromise;
|
|
727
|
+
};
|
|
728
|
+
await handler(req, res, wrappedNext);
|
|
729
|
+
if (!nextCalled) {
|
|
730
|
+
if (res.isEnded()) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
await Promise.race([nextSignal, res.waitForEnd()]);
|
|
734
|
+
if (!nextCalled || res.isEnded()) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return nextPromise ? await nextPromise : true;
|
|
739
|
+
};
|
|
740
|
+
return dispatch();
|
|
741
|
+
}
|