@abejarano/ts-express-server 1.7.5 → 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.
@@ -25,6 +25,7 @@ export interface ServerResponse {
25
25
  send(body: unknown): void | Promise<void>;
26
26
  set(name: string, value: string): this;
27
27
  header(name: string, value: string): this;
28
+ setHeader?(name: string, value: string): this;
28
29
  cookie?(name: string, value: string, options?: {
29
30
  maxAge?: number;
30
31
  domain?: string;
@@ -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.get("trustProxy"));
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 undefined;
976
+ return baseDefaults;
914
977
  }
915
978
  const defaults = input;
916
979
  if (!defaults.options || typeof defaults.options !== "object") {
917
- return undefined;
980
+ return baseDefaults;
918
981
  }
919
- return defaults;
982
+ return {
983
+ applyTo: defaults.applyTo ?? baseDefaults.applyTo,
984
+ options: { ...baseDefaults.options, ...defaults.options },
985
+ };
920
986
  }
921
- function resolveTrustProxySetting(input) {
922
- if (input === true || input === false) {
923
- return input;
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 () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@abejarano/ts-express-server",
3
3
  "author": "angel bejarano / angel.bejarano@jaspesoft.com",
4
- "version": "1.7.5",
4
+ "version": "1.7.6",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [