@abejarano/ts-express-server 1.7.4 → 1.7.6
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type fileUpload from "express-fileupload";
|
|
1
2
|
export declare enum ServerRuntime {
|
|
2
3
|
Express = "express",
|
|
3
4
|
Bun = "bun"
|
|
@@ -12,7 +13,7 @@ export interface ServerRequest {
|
|
|
12
13
|
query: Record<string, string | string[]>;
|
|
13
14
|
headers: Record<string, string | string[] | undefined>;
|
|
14
15
|
body?: unknown;
|
|
15
|
-
files?:
|
|
16
|
+
files?: ServerFiles;
|
|
16
17
|
cookies?: Record<string, string>;
|
|
17
18
|
ip?: string;
|
|
18
19
|
requestId?: string;
|
|
@@ -24,6 +25,7 @@ export interface ServerResponse {
|
|
|
24
25
|
send(body: unknown): void | Promise<void>;
|
|
25
26
|
set(name: string, value: string): this;
|
|
26
27
|
header(name: string, value: string): this;
|
|
28
|
+
setHeader?(name: string, value: string): this;
|
|
27
29
|
cookie?(name: string, value: string, options?: {
|
|
28
30
|
maxAge?: number;
|
|
29
31
|
domain?: string;
|
|
@@ -36,6 +38,18 @@ export interface ServerResponse {
|
|
|
36
38
|
download?(path: string, filename?: string, callback?: (err?: unknown) => void): this;
|
|
37
39
|
end(body?: unknown): void | Promise<void>;
|
|
38
40
|
}
|
|
41
|
+
export type BunMultipartFile = {
|
|
42
|
+
name: string;
|
|
43
|
+
size: number;
|
|
44
|
+
type: string;
|
|
45
|
+
slice?: (start?: number, end?: number) => {
|
|
46
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
47
|
+
};
|
|
48
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
49
|
+
lastModified?: number;
|
|
50
|
+
};
|
|
51
|
+
export type ServerFile = BunMultipartFile | fileUpload.UploadedFile;
|
|
52
|
+
export type ServerFiles = Record<string, ServerFile | ServerFile[]>;
|
|
39
53
|
export type ServerHandlerInput = ServerHandler | ServerHandler[] | ServerRouter;
|
|
40
54
|
export interface ServerRouter {
|
|
41
55
|
use(pathOrHandler: string | ServerHandlerInput, ...handlers: ServerHandlerInput[]): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerAdapter, ServerApp, ServerInstance, ServerRequest, ServerRouter, ServerRuntime } from "../abstract/ServerTypes";
|
|
1
|
+
import { ServerAdapter, ServerApp, ServerInstance, ServerRequest, ServerRouter, ServerRuntime, BunMultipartFile } from "../abstract/ServerTypes";
|
|
2
2
|
export declare class BunAdapter implements ServerAdapter {
|
|
3
3
|
runtime: ServerRuntime;
|
|
4
4
|
createApp(): ServerApp;
|
|
@@ -6,16 +6,7 @@ export declare class BunAdapter implements ServerAdapter {
|
|
|
6
6
|
configure(app: ServerApp, _port: number): void;
|
|
7
7
|
listen(app: ServerApp, port: number, onListen: () => void): ServerInstance;
|
|
8
8
|
}
|
|
9
|
-
type MultipartFile =
|
|
10
|
-
name: string;
|
|
11
|
-
size: number;
|
|
12
|
-
type: string;
|
|
13
|
-
slice?: (start?: number, end?: number) => {
|
|
14
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
15
|
-
};
|
|
16
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
17
|
-
lastModified?: number;
|
|
18
|
-
};
|
|
9
|
+
type MultipartFile = BunMultipartFile;
|
|
19
10
|
export declare function getFiles(req: ServerRequest, field: string): MultipartFile[];
|
|
20
11
|
export declare function getFile(req: ServerRequest, field: string): MultipartFile | undefined;
|
|
21
12
|
export {};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.BunAdapter = void 0;
|
|
4
7
|
exports.getFiles = getFiles;
|
|
5
8
|
exports.getFile = getFile;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
6
11
|
const ServerTypes_1 = require("../abstract/ServerTypes");
|
|
7
12
|
class BunResponse {
|
|
8
|
-
constructor(cookieJar, handlerTimeoutMs, cookieDefaults) {
|
|
13
|
+
constructor(cookieJar, handlerTimeoutMs, cookieDefaults, downloadRoot) {
|
|
9
14
|
this.statusCode = 200;
|
|
10
15
|
this.statusExplicitlySet = false;
|
|
11
16
|
this.headers = new Headers();
|
|
@@ -15,6 +20,7 @@ class BunResponse {
|
|
|
15
20
|
this.cookieJar = cookieJar;
|
|
16
21
|
this.handlerTimeoutMs = handlerTimeoutMs;
|
|
17
22
|
this.cookieDefaults = cookieDefaults;
|
|
23
|
+
this.downloadRoot = downloadRoot ?? DEFAULT_DOWNLOAD_ROOT;
|
|
18
24
|
this.endPromise = new Promise((resolve) => {
|
|
19
25
|
this.resolveEnd = resolve;
|
|
20
26
|
});
|
|
@@ -44,6 +50,9 @@ class BunResponse {
|
|
|
44
50
|
header(name, value) {
|
|
45
51
|
return this.set(name, value);
|
|
46
52
|
}
|
|
53
|
+
setHeader(name, value) {
|
|
54
|
+
return this.set(name, value);
|
|
55
|
+
}
|
|
47
56
|
cookie(name, value, options = {}) {
|
|
48
57
|
if (this.ended) {
|
|
49
58
|
return this;
|
|
@@ -110,6 +119,39 @@ class BunResponse {
|
|
|
110
119
|
this.ended = true;
|
|
111
120
|
this.resolveEnd?.();
|
|
112
121
|
}
|
|
122
|
+
download(filePath, filename, callback) {
|
|
123
|
+
if (this.ended) {
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const safePath = resolveSafeDownloadPath(this.downloadRoot, filePath);
|
|
128
|
+
const stat = (0, fs_1.statSync)(safePath);
|
|
129
|
+
if (!stat.isFile()) {
|
|
130
|
+
const error = new Error("File not found");
|
|
131
|
+
callback?.(error);
|
|
132
|
+
if (!this.ended) {
|
|
133
|
+
this.status(404).json({ message: "File not found" });
|
|
134
|
+
}
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
const resolvedName = filename && filename.trim().length > 0
|
|
138
|
+
? filename
|
|
139
|
+
: path_1.default.basename(safePath);
|
|
140
|
+
this.headers.set("content-disposition", `attachment; filename="${sanitizeFilename(resolvedName)}"`);
|
|
141
|
+
this.rawResponse = new Response(Bun.file(safePath));
|
|
142
|
+
this.ended = true;
|
|
143
|
+
this.resolveEnd?.();
|
|
144
|
+
callback?.();
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
callback?.(error);
|
|
149
|
+
if (!this.ended) {
|
|
150
|
+
this.status(500).json({ message: "File download failed" });
|
|
151
|
+
}
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
113
155
|
isEnded() {
|
|
114
156
|
return this.ended;
|
|
115
157
|
}
|
|
@@ -324,7 +366,7 @@ class BunApp extends BunRouter {
|
|
|
324
366
|
: typeof handlerTimeoutSetting === "number"
|
|
325
367
|
? handlerTimeoutSetting
|
|
326
368
|
: undefined;
|
|
327
|
-
const trustProxy = resolveTrustProxySetting(this
|
|
369
|
+
const trustProxy = resolveTrustProxySetting(this);
|
|
328
370
|
const maxConcurrentRequests = Number(this.get("maxConcurrentRequests") ?? 0);
|
|
329
371
|
if (maxConcurrentRequests > 0 && this.activeRequests >= maxConcurrentRequests) {
|
|
330
372
|
return new Response(JSON.stringify({ message: "Server busy" }), {
|
|
@@ -334,7 +376,7 @@ class BunApp extends BunRouter {
|
|
|
334
376
|
}
|
|
335
377
|
this.activeRequests += 1;
|
|
336
378
|
const req = createRequest(request, client?.address, trustProxy);
|
|
337
|
-
const res = new BunResponse(cookieJar, handlerTimeoutMs, resolveCookieDefaults(this.get("cookieDefaults")));
|
|
379
|
+
const res = new BunResponse(cookieJar, handlerTimeoutMs, resolveCookieDefaults(this.get("cookieDefaults")), resolveDownloadRoot(this.get("downloadRoot")));
|
|
338
380
|
try {
|
|
339
381
|
await this.handle(req, res, () => undefined);
|
|
340
382
|
}
|
|
@@ -487,6 +529,8 @@ const createMultipartBodyParser = (app) => {
|
|
|
487
529
|
const fields = {};
|
|
488
530
|
const files = {};
|
|
489
531
|
let fileCount = 0;
|
|
532
|
+
let fieldCount = 0;
|
|
533
|
+
let fieldBytes = 0;
|
|
490
534
|
for (const [key, value] of formData.entries()) {
|
|
491
535
|
if (isFile(value)) {
|
|
492
536
|
if (value.size > options.maxFileBytes) {
|
|
@@ -530,6 +574,17 @@ const createMultipartBodyParser = (app) => {
|
|
|
530
574
|
}
|
|
531
575
|
const existing = fields[key];
|
|
532
576
|
const textValue = String(value);
|
|
577
|
+
fieldCount += 1;
|
|
578
|
+
if (fieldCount > options.maxFields) {
|
|
579
|
+
res.status(413).json({ message: "Payload too large" });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const textBytes = Buffer.byteLength(textValue, "utf8");
|
|
583
|
+
fieldBytes += textBytes;
|
|
584
|
+
if (textBytes > options.maxFieldBytes || fieldBytes > options.maxFieldsBytes) {
|
|
585
|
+
res.status(413).json({ message: "Payload too large" });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
533
588
|
if (existing === undefined) {
|
|
534
589
|
fields[key] = textValue;
|
|
535
590
|
}
|
|
@@ -598,6 +653,9 @@ const DEFAULT_MULTIPART_OPTIONS = {
|
|
|
598
653
|
maxBodyBytes: 10 * 1024 * 1024,
|
|
599
654
|
maxFileBytes: 10 * 1024 * 1024,
|
|
600
655
|
maxFiles: 10,
|
|
656
|
+
maxFields: 200,
|
|
657
|
+
maxFieldBytes: 64 * 1024,
|
|
658
|
+
maxFieldsBytes: 512 * 1024,
|
|
601
659
|
};
|
|
602
660
|
function serializeCookie(name, value, options) {
|
|
603
661
|
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
@@ -670,6 +728,9 @@ function normalizeMultipartOptions(input) {
|
|
|
670
728
|
maxBodyBytes: value.maxBodyBytes ?? DEFAULT_MULTIPART_OPTIONS.maxBodyBytes,
|
|
671
729
|
maxFileBytes: value.maxFileBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFileBytes,
|
|
672
730
|
maxFiles: value.maxFiles ?? DEFAULT_MULTIPART_OPTIONS.maxFiles,
|
|
731
|
+
maxFields: value.maxFields ?? DEFAULT_MULTIPART_OPTIONS.maxFields,
|
|
732
|
+
maxFieldBytes: value.maxFieldBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFieldBytes,
|
|
733
|
+
maxFieldsBytes: value.maxFieldsBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFieldsBytes,
|
|
673
734
|
allowedMimeTypes: value.allowedMimeTypes,
|
|
674
735
|
allowedFileSignatures: value.allowedFileSignatures,
|
|
675
736
|
validateFile: value.validateFile,
|
|
@@ -734,6 +795,7 @@ function parseContentLength(header) {
|
|
|
734
795
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
735
796
|
}
|
|
736
797
|
const DEFAULT_HANDLER_TIMEOUT_MS = 30000;
|
|
798
|
+
const DEFAULT_DOWNLOAD_ROOT = process.cwd();
|
|
737
799
|
function readSetCookieHeaders(headers) {
|
|
738
800
|
const bunHeaders = headers;
|
|
739
801
|
const setCookieFromApi = bunHeaders.getSetCookie?.() ??
|
|
@@ -909,18 +971,31 @@ function applyCookieDefaults(options, defaults) {
|
|
|
909
971
|
return { ...defaults.options, ...options };
|
|
910
972
|
}
|
|
911
973
|
function resolveCookieDefaults(input) {
|
|
974
|
+
const baseDefaults = getDefaultCookieDefaults();
|
|
912
975
|
if (!input || typeof input !== "object") {
|
|
913
|
-
return
|
|
976
|
+
return baseDefaults;
|
|
914
977
|
}
|
|
915
978
|
const defaults = input;
|
|
916
979
|
if (!defaults.options || typeof defaults.options !== "object") {
|
|
917
|
-
return
|
|
980
|
+
return baseDefaults;
|
|
918
981
|
}
|
|
919
|
-
return
|
|
982
|
+
return {
|
|
983
|
+
applyTo: defaults.applyTo ?? baseDefaults.applyTo,
|
|
984
|
+
options: { ...baseDefaults.options, ...defaults.options },
|
|
985
|
+
};
|
|
920
986
|
}
|
|
921
|
-
function resolveTrustProxySetting(
|
|
922
|
-
|
|
923
|
-
|
|
987
|
+
function resolveTrustProxySetting(app) {
|
|
988
|
+
const input = app.get("trustProxy");
|
|
989
|
+
const allowInsecure = app.get("allowInsecureTrustProxy") === true ||
|
|
990
|
+
process?.env?.ALLOW_INSECURE_TRUST_PROXY === "true";
|
|
991
|
+
if (input === true) {
|
|
992
|
+
if (!allowInsecure) {
|
|
993
|
+
throw new Error("Invalid trustProxy=true. Use CIDR allowlist or set allowInsecureTrustProxy=true explicitly.");
|
|
994
|
+
}
|
|
995
|
+
return true;
|
|
996
|
+
}
|
|
997
|
+
if (input === false) {
|
|
998
|
+
return false;
|
|
924
999
|
}
|
|
925
1000
|
if (Array.isArray(input) && input.every((entry) => typeof entry === "string")) {
|
|
926
1001
|
return input;
|
|
@@ -937,6 +1012,19 @@ function shouldEnableSecurityHeaders(setting) {
|
|
|
937
1012
|
if (typeof setting === "boolean") {
|
|
938
1013
|
return setting;
|
|
939
1014
|
}
|
|
1015
|
+
return isProduction();
|
|
1016
|
+
}
|
|
1017
|
+
function getDefaultCookieDefaults() {
|
|
1018
|
+
return {
|
|
1019
|
+
applyTo: "session",
|
|
1020
|
+
options: {
|
|
1021
|
+
httpOnly: true,
|
|
1022
|
+
sameSite: "lax",
|
|
1023
|
+
secure: isProduction(),
|
|
1024
|
+
},
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function isProduction() {
|
|
940
1028
|
return process?.env?.NODE_ENV === "production";
|
|
941
1029
|
}
|
|
942
1030
|
function createSecurityHeadersMiddleware(_app) {
|
|
@@ -1019,6 +1107,26 @@ function matchesSignature(buffer, kind) {
|
|
|
1019
1107
|
function isValidCookieName(name) {
|
|
1020
1108
|
return /^[!#$%&'*+\-.^_|~0-9A-Za-z]+$/.test(name);
|
|
1021
1109
|
}
|
|
1110
|
+
function sanitizeFilename(value) {
|
|
1111
|
+
return value.replace(/[/\\"]/g, "_");
|
|
1112
|
+
}
|
|
1113
|
+
function resolveDownloadRoot(input) {
|
|
1114
|
+
if (typeof input === "string" && input.trim().length > 0) {
|
|
1115
|
+
return path_1.default.resolve(input);
|
|
1116
|
+
}
|
|
1117
|
+
return DEFAULT_DOWNLOAD_ROOT;
|
|
1118
|
+
}
|
|
1119
|
+
function resolveSafeDownloadPath(root, inputPath) {
|
|
1120
|
+
const resolvedRoot = path_1.default.resolve(root);
|
|
1121
|
+
const resolved = path_1.default.resolve(resolvedRoot, inputPath);
|
|
1122
|
+
const rootPrefix = resolvedRoot === path_1.default.parse(resolvedRoot).root
|
|
1123
|
+
? resolvedRoot
|
|
1124
|
+
: `${resolvedRoot}${path_1.default.sep}`;
|
|
1125
|
+
if (!resolved.startsWith(rootPrefix)) {
|
|
1126
|
+
throw new Error("Invalid path");
|
|
1127
|
+
}
|
|
1128
|
+
return resolved;
|
|
1129
|
+
}
|
|
1022
1130
|
async function runHandlers(handlers, req, res) {
|
|
1023
1131
|
let index = 0;
|
|
1024
1132
|
const dispatch = async () => {
|