skyguard-js 1.1.0 → 1.1.2

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.
Files changed (66) hide show
  1. package/README.md +43 -33
  2. package/dist/app.d.ts +41 -7
  3. package/dist/app.js +51 -13
  4. package/dist/crypto/jwt.js +2 -2
  5. package/dist/helpers/http.d.ts +1 -2
  6. package/dist/helpers/http.js +2 -2
  7. package/dist/http/logger.js +1 -1
  8. package/dist/http/response.d.ts +3 -3
  9. package/dist/http/response.js +11 -6
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +4 -1
  12. package/dist/middlewares/cors.js +1 -1
  13. package/dist/parsers/multipartParser.d.ts +11 -0
  14. package/dist/parsers/multipartParser.js +43 -13
  15. package/dist/routing/router.js +16 -8
  16. package/dist/sessions/fileSessionStorage.js +4 -5
  17. package/dist/sessions/sessionStorage.d.ts +1 -1
  18. package/dist/static/contentDisposition.js +6 -1
  19. package/dist/static/fileStaticHandler.js +1 -1
  20. package/dist/storage/storage.js +1 -4
  21. package/dist/validators/rules/arrayRule.d.ts +11 -0
  22. package/dist/validators/rules/arrayRule.js +40 -0
  23. package/dist/validators/rules/bigIntRule.d.ts +20 -0
  24. package/dist/validators/rules/bigIntRule.js +53 -0
  25. package/dist/validators/rules/booleanRule.d.ts +1 -1
  26. package/dist/validators/rules/booleanRule.js +2 -2
  27. package/dist/validators/rules/dateRule.d.ts +1 -1
  28. package/dist/validators/rules/index.d.ts +5 -2
  29. package/dist/validators/rules/index.js +11 -5
  30. package/dist/validators/rules/literalRule.d.ts +7 -0
  31. package/dist/validators/rules/literalRule.js +18 -0
  32. package/dist/validators/rules/numberRule.d.ts +2 -1
  33. package/dist/validators/rules/numberRule.js +7 -6
  34. package/dist/validators/rules/objectRule.d.ts +7 -0
  35. package/dist/validators/rules/objectRule.js +27 -0
  36. package/dist/validators/rules/stringRule.d.ts +68 -3
  37. package/dist/validators/rules/stringRule.js +135 -9
  38. package/dist/validators/rules/{requiredRule.d.ts → unionRule.d.ts} +3 -7
  39. package/dist/validators/rules/unionRule.js +31 -0
  40. package/dist/validators/types.d.ts +2 -5
  41. package/dist/validators/validationRule.d.ts +8 -25
  42. package/dist/validators/validationRule.js +12 -23
  43. package/dist/validators/validationSchema.d.ts +71 -22
  44. package/dist/validators/validationSchema.js +104 -50
  45. package/dist/validators/validator.d.ts +3 -3
  46. package/dist/validators/validator.js +18 -9
  47. package/dist/views/engineTemplate.d.ts +56 -0
  48. package/dist/views/engineTemplate.js +64 -0
  49. package/package.json +9 -13
  50. package/dist/helpers/app.d.ts +0 -4
  51. package/dist/helpers/app.js +0 -12
  52. package/dist/helpers/index.d.ts +0 -1
  53. package/dist/helpers/index.js +0 -9
  54. package/dist/validators/rules/emailRule.d.ts +0 -12
  55. package/dist/validators/rules/emailRule.js +0 -24
  56. package/dist/validators/rules/requiredRule.js +0 -21
  57. package/dist/views/helpersTemplate.d.ts +0 -104
  58. package/dist/views/helpersTemplate.js +0 -186
  59. package/dist/views/index.d.ts +0 -4
  60. package/dist/views/index.js +0 -9
  61. package/dist/views/raptorEngine.d.ts +0 -127
  62. package/dist/views/raptorEngine.js +0 -165
  63. package/dist/views/templateEngine.d.ts +0 -80
  64. package/dist/views/templateEngine.js +0 -204
  65. package/dist/views/view.d.ts +0 -55
  66. package/dist/views/view.js +0 -2
package/README.md CHANGED
@@ -35,6 +35,11 @@ At its current stage, the framework focuses on **routing**, **internal architect
35
35
 
36
36
  ---
37
37
 
38
+ > [!NOTE]
39
+ > It is recommended to develop with `TypeScript` for a more secure and efficient development process; the framework already has native support for `TypeScript` and includes the necessary types.
40
+
41
+ ---
42
+
38
43
  ## 📦 Installation
39
44
 
40
45
  ```bash
@@ -160,17 +165,17 @@ app.staticFiles(join(__dirname, "..", "static"));
160
165
 
161
166
  ## ⛔ Data Validation
162
167
 
163
- Skyguard.js provides a **declarative validation system** using schemas.
168
+ To validate data in the body of client requests, the framework provides the creation of validation schemas, which are created as follows:
164
169
 
165
170
  ```ts
166
- import { validator } from "skyguard-js/validation";
167
-
168
- const userSchema = validator.schema({
169
- name: validator.string({ maxLength: 60 }),
170
- email: validator.email().required(),
171
- age: validator.number({ min: 18 }),
172
- active: validator.boolean().required(),
173
- birthdate: validator.date({ max: new Date() }),
171
+ import { v, schema } from "skyguard-js";
172
+
173
+ const userSchema = schema({
174
+ name: v.string({ maxLength: 60 }),
175
+ email: v.email(),
176
+ age: v.number({ min: 18 }),
177
+ active: v.boolean().default(false),
178
+ birthdate: v.date({ max: new Date() }),
174
179
  });
175
180
 
176
181
  app.post("/users", (request: Request) => {
@@ -183,6 +188,8 @@ app.post("/users", (request: Request) => {
183
188
  });
184
189
  ```
185
190
 
191
+ By default each property you define in the schema is required, to define it optional you use the `.optional()` or `.default(value)` function
192
+
186
193
  Validation is:
187
194
 
188
195
  - Fail-fast per field
@@ -331,43 +338,46 @@ app.post(
331
338
 
332
339
  ## 📄 Views & Template Engine
333
340
 
334
- To render HTML views, use the `render` helper.
341
+ To render views, you must first set up the template engine using the `engineTemplates` method of the `app`, set the view path with the `views` method of the `app`, and then you can use the `render` method within your handlers to render the views with the data you want to pass.
335
342
 
336
343
  ```ts
337
- import { render } from "skyguard-js/helpers";
344
+ import { engine } from "express-handlebars";
345
+ import ejs from "ejs";
346
+ import { join } from "node:path";
347
+
348
+ app.views(__dirname, "views");
349
+
350
+ // Config for Express Handlebars
351
+ app.engineTemplates(
352
+ "hbs",
353
+ engine({
354
+ extname: "hbs",
355
+ layoutsDir: join(__dirname, "views"),
356
+ defaultLayout: "main",
357
+ }),
358
+ );
359
+
360
+ // Config for EJS
361
+ app.engineTemplates("ejs", (templatePath, data) => {
362
+ return ejs.renderFile(templatePath, data);
363
+ });
338
364
 
339
365
  app.get("/home", () => {
340
- return render(
341
- "home",
342
- {
343
- title: "Products",
344
- products: [
345
- { name: "Laptop", price: 999.99 },
346
- { name: "Mouse", price: 29.99 },
347
- ],
348
- user: { name: "John", role: "admin" },
349
- },
350
- "main",
351
- );
366
+ return render("index", {
367
+ title: "Home Page",
368
+ message: "Welcome to the home page!",
369
+ });
352
370
  });
353
371
  ```
354
372
 
355
- ### Supported features
356
-
357
- - Variable interpolation (`{{ variable }}`)
358
- - Conditionals (`{{#if}}`)
359
- - Loops (`{{#each}}`)
360
- - Layouts
361
- - Partials
362
- - Built-in helpers (`upper`, `lower`, `date`)
363
- - Custom helpers
373
+ Currently, it works with third-party template engines such as **Express Handlebars**, **Pug**, and **EJS**, but the idea is to implement its own template engine in the future.
364
374
 
365
375
  ---
366
376
 
367
377
  ## 🔮 Roadmap (Tentative)
368
378
 
369
379
  - Middleware system (✅)
370
- - Template engine (✅)
380
+ - Template engines supported (✅)
371
381
  - Request / Response abstraction (✅)
372
382
  - Data validation (✅)
373
383
  - Error handling improvements (✅)
package/dist/app.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RouterGroup } from "./routing";
2
- import { type View } from "./views";
3
2
  import type { Middleware, RouteHandler } from "./types";
3
+ import { type TemplateEngineFunction } from "./views/engineTemplate";
4
4
  /**
5
5
  * The `App` class acts as the **execution kernel** and **lifecycle orchestrator**
6
6
  * of the framework.
@@ -22,16 +22,14 @@ import type { Middleware, RouteHandler } from "./types";
22
22
  export declare class App {
23
23
  /** Main routing system */
24
24
  private router;
25
- /**
26
- * View engine used to render templates.
27
- *
28
- * Typically consumed inside controllers to generate HTML responses.
29
- */
30
- view: View;
31
25
  /** Static file handler (optional) */
32
26
  private staticFileHandler;
27
+ /** Logger instance for request logging */
33
28
  private logger;
29
+ /** Timestamp marking the start of request processing (for logging) */
34
30
  private startTime;
31
+ /** View engine for rendering templates (optional) */
32
+ private viewEngine;
35
33
  /**
36
34
  * Bootstraps and configures the application.
37
35
  *
@@ -67,6 +65,42 @@ export declare class App {
67
65
  * // /public/css/style.css → /css/style.css
68
66
  */
69
67
  staticFiles(publicPath: string): void;
68
+ /**
69
+ * Configures the views directory for template rendering.
70
+ *
71
+ * @param pathSegments Path segments leading to the views directory
72
+ * @example
73
+ * app.views(__dirname, "views");
74
+ */
75
+ views(...pathSegments: string[]): void;
76
+ /**
77
+ * Configures the template engine for rendering views.
78
+ *
79
+ * @param extension - File extension associated with the template engine (e.g. "hbs", "ejs")
80
+ * @param engine - Function that renders a template given its path and data
81
+ *
82
+ * @example
83
+ * app.engineTemplates("hbs", (templatePath, data) => {
84
+ * const content = fs.readFileSync(templatePath, "utf-8");
85
+ * return content.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
86
+ * return data[key] ?? "";
87
+ * });
88
+ * });
89
+ *
90
+ * // With express-handlebars
91
+ * app.engineTemplates(
92
+ * "hbs",
93
+ * engine({
94
+ * extname: "hbs",
95
+ * layoutsDir: join(__dirname, "views"),
96
+ * defaultLayout: "main",
97
+ * }),
98
+ * );
99
+ *
100
+ * The `extension` parameter allows the framework to automatically select the correct
101
+ * template engine based on the file extension of the view being rendered.
102
+ */
103
+ engineTemplates(extension: string, engine: TemplateEngineFunction): void;
70
104
  /**
71
105
  * Starts the HTTP server on the given port.
72
106
  *
package/dist/app.js CHANGED
@@ -4,12 +4,12 @@ exports.createApp = exports.App = void 0;
4
4
  const routing_1 = require("./routing");
5
5
  const http_1 = require("./http");
6
6
  const validationException_1 = require("./exceptions/validationException");
7
- const views_1 = require("./views");
8
7
  const node_path_1 = require("node:path");
9
- const app_1 = require("./helpers/app");
10
8
  const fileStaticHandler_1 = require("./static/fileStaticHandler");
11
9
  const node_http_1 = require("node:http");
12
10
  const httpExceptions_1 = require("./exceptions/httpExceptions");
11
+ const engineTemplate_1 = require("./views/engineTemplate");
12
+ const container_1 = require("./container/container");
13
13
  /**
14
14
  * The `App` class acts as the **execution kernel** and **lifecycle orchestrator**
15
15
  * of the framework.
@@ -31,16 +31,14 @@ const httpExceptions_1 = require("./exceptions/httpExceptions");
31
31
  class App {
32
32
  /** Main routing system */
33
33
  router;
34
- /**
35
- * View engine used to render templates.
36
- *
37
- * Typically consumed inside controllers to generate HTML responses.
38
- */
39
- view;
40
34
  /** Static file handler (optional) */
41
35
  staticFileHandler = null;
36
+ /** Logger instance for request logging */
42
37
  logger;
38
+ /** Timestamp marking the start of request processing (for logging) */
43
39
  startTime;
40
+ /** View engine for rendering templates (optional) */
41
+ viewEngine;
44
42
  /**
45
43
  * Bootstraps and configures the application.
46
44
  *
@@ -51,10 +49,10 @@ class App {
51
49
  * @returns The singleton `App` instance
52
50
  */
53
51
  static bootstrap() {
54
- const app = (0, app_1.singleton)(App);
52
+ const app = container_1.Container.singleton(App);
55
53
  app.router = new routing_1.Router();
56
- app.view = new views_1.RaptorEngine((0, node_path_1.join)(__dirname, "..", "views"));
57
54
  app.logger = new http_1.Logger();
55
+ app.viewEngine = container_1.Container.singleton(engineTemplate_1.ViewEngine);
58
56
  return app;
59
57
  }
60
58
  /**
@@ -100,6 +98,46 @@ class App {
100
98
  staticFiles(publicPath) {
101
99
  this.staticFileHandler = new fileStaticHandler_1.StaticFileHandler(publicPath);
102
100
  }
101
+ /**
102
+ * Configures the views directory for template rendering.
103
+ *
104
+ * @param pathSegments Path segments leading to the views directory
105
+ * @example
106
+ * app.views(__dirname, "views");
107
+ */
108
+ views(...pathSegments) {
109
+ this.viewEngine.setViewsPath((0, node_path_1.join)(...pathSegments));
110
+ }
111
+ /**
112
+ * Configures the template engine for rendering views.
113
+ *
114
+ * @param extension - File extension associated with the template engine (e.g. "hbs", "ejs")
115
+ * @param engine - Function that renders a template given its path and data
116
+ *
117
+ * @example
118
+ * app.engineTemplates("hbs", (templatePath, data) => {
119
+ * const content = fs.readFileSync(templatePath, "utf-8");
120
+ * return content.replace(/{{\s*(\w+)\s*}}/g, (_, key) => {
121
+ * return data[key] ?? "";
122
+ * });
123
+ * });
124
+ *
125
+ * // With express-handlebars
126
+ * app.engineTemplates(
127
+ * "hbs",
128
+ * engine({
129
+ * extname: "hbs",
130
+ * layoutsDir: join(__dirname, "views"),
131
+ * defaultLayout: "main",
132
+ * }),
133
+ * );
134
+ *
135
+ * The `extension` parameter allows the framework to automatically select the correct
136
+ * template engine based on the file extension of the view being rendered.
137
+ */
138
+ engineTemplates(extension, engine) {
139
+ this.viewEngine.setEngine(extension, engine);
140
+ }
103
141
  /**
104
142
  * Starts the HTTP server on the given port.
105
143
  *
@@ -177,19 +215,19 @@ class App {
177
215
  */
178
216
  handleError(error, adapter) {
179
217
  if (error instanceof httpExceptions_1.HttpException) {
180
- adapter.sendResponse(http_1.Response.json(error.toJSON()).setStatus(error.statusCode));
218
+ adapter.sendResponse(http_1.Response.json(error.toJSON()).setStatusCode(error.statusCode));
181
219
  return;
182
220
  }
183
221
  if (error instanceof validationException_1.ValidationException) {
184
222
  adapter.sendResponse(http_1.Response.json({
185
223
  errors: error.getErrorsByField(),
186
- }).setStatus(400));
224
+ }).setStatusCode(400));
187
225
  return;
188
226
  }
189
227
  adapter.sendResponse(http_1.Response.json({
190
228
  statusCode: 500,
191
229
  message: "Internal Server Error",
192
- }).setStatus(500));
230
+ }).setStatusCode(500));
193
231
  console.error(error);
194
232
  }
195
233
  }
@@ -85,7 +85,7 @@ const verifyJWT = (token, secret) => {
85
85
  return null;
86
86
  return payload;
87
87
  }
88
- catch (error) {
88
+ catch {
89
89
  return null;
90
90
  }
91
91
  };
@@ -105,7 +105,7 @@ const decodeJWT = (token) => {
105
105
  const payload = JSON.parse(base64UrlDecode(parts[1]));
106
106
  return { header, payload };
107
107
  }
108
- catch (error) {
108
+ catch {
109
109
  return null;
110
110
  }
111
111
  };
@@ -1,4 +1,3 @@
1
- import type { TemplateContext } from "../types";
2
1
  import { Response } from "../http/response";
3
2
  /**
4
3
  * Creates an HTTP response with a JSON body.
@@ -56,4 +55,4 @@ export declare function download(path: string, filename?: string, headers?: Reco
56
55
  * @example
57
56
  * return await render("users/profile", { user }, "main");
58
57
  */
59
- export declare function render(view: string, params: TemplateContext, layout?: string): Promise<Response>;
58
+ export declare function render(data: string, params?: Record<string, unknown>): Promise<Response>;
@@ -70,6 +70,6 @@ async function download(path, filename, headers) {
70
70
  * @example
71
71
  * return await render("users/profile", { user }, "main");
72
72
  */
73
- async function render(view, params, layout) {
74
- return await response_1.Response.render(view, params, layout);
73
+ async function render(data, params) {
74
+ return await response_1.Response.render(data, params);
75
75
  }
@@ -13,7 +13,7 @@ class Logger {
13
13
  const diff = process.hrtime.bigint() - startTime;
14
14
  const responseTime = (Number(diff) / 1_000_000).toFixed(3);
15
15
  const coloredStatus = this.colorizeStatus(res.statusCode);
16
- const logLine = `${method} ${url} ${coloredStatus} ${responseTime} ms - ${contentLength}`;
16
+ const logLine = `${method} ${url} ${coloredStatus} ${responseTime} ms - ${contentLength.toString()}`;
17
17
  this.stream.write(logLine + "\n");
18
18
  }
19
19
  colorizeStatus(statusCode) {
@@ -1,4 +1,4 @@
1
- import type { Headers, TemplateContext } from "../types";
1
+ import type { Headers } from "../types";
2
2
  /**
3
3
  * Represents an outgoing response sent to the client.
4
4
  *
@@ -29,7 +29,7 @@ export declare class Response {
29
29
  */
30
30
  private _content;
31
31
  get statusCode(): number;
32
- setStatus(newStatus: number): this;
32
+ setStatusCode(newStatus: number): this;
33
33
  get headers(): Headers;
34
34
  setHeaders(headers: Headers): this;
35
35
  private merge;
@@ -115,5 +115,5 @@ export declare class Response {
115
115
  * return Response.render("users/profile", { user });
116
116
  * return Response.render("auth/login", {}, "auth");
117
117
  */
118
- static render(view: string, params: TemplateContext, layout?: string): Promise<Response>;
118
+ static render(data: string, params?: Record<string, unknown>): Promise<Response>;
119
119
  }
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Response = void 0;
4
4
  const statusCodes_1 = require("./statusCodes");
5
5
  const invalidHttpStatusException_1 = require("../exceptions/invalidHttpStatusException");
6
- const app_1 = require("../helpers/app");
7
6
  const fileDownload_1 = require("../static/fileDownload");
7
+ const engineTemplate_1 = require("../views/engineTemplate");
8
+ const container_1 = require("../container/container");
8
9
  /**
9
10
  * Represents an outgoing response sent to the client.
10
11
  *
@@ -37,7 +38,7 @@ class Response {
37
38
  get statusCode() {
38
39
  return this._statusCode;
39
40
  }
40
- setStatus(newStatus) {
41
+ setStatusCode(newStatus) {
41
42
  const status = statusCodes_1.statusCodes[newStatus] ?? null;
42
43
  if (!status)
43
44
  throw new invalidHttpStatusException_1.InvalidHttpStatusException(newStatus);
@@ -155,7 +156,7 @@ class Response {
155
156
  * });
156
157
  */
157
158
  static redirect(url) {
158
- return new this().setStatus(302).setHeader("location", url);
159
+ return new this().setStatusCode(302).setHeader("location", url);
159
160
  }
160
161
  static async download(path, filename, headers) {
161
162
  const downloadClass = new fileDownload_1.FileDownloadHelper();
@@ -176,9 +177,13 @@ class Response {
176
177
  * return Response.render("users/profile", { user });
177
178
  * return Response.render("auth/login", {}, "auth");
178
179
  */
179
- static async render(view, params, layout = null) {
180
- const content = await (0, app_1.app)().view.render(view, params, layout);
181
- return new this().setContentType("text/html").setContent(content);
180
+ static async render(data, params) {
181
+ const viewEngine = container_1.Container.resolve(engineTemplate_1.ViewEngine);
182
+ if (viewEngine.hasEngine())
183
+ data = await viewEngine.render(data, params ?? {});
184
+ return new this()
185
+ .setContentType("text/html; charset=utf-8")
186
+ .setContent(Buffer.from(data, "utf-8"));
182
187
  }
183
188
  }
184
189
  exports.Response = Response;
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export { FileSessionStorage, MemorySessionStorage } from "./sessions";
6
6
  export { HttpMethods } from "./http/httpMethods";
7
7
  export { createUploader } from "./storage/uploader";
8
8
  export { StorageType } from "./storage/types";
9
+ export { v, schema } from "./validators/validationSchema";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StorageType = exports.createUploader = exports.HttpMethods = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Response = exports.Request = exports.createApp = void 0;
3
+ exports.schema = exports.v = exports.StorageType = exports.createUploader = exports.HttpMethods = exports.MemorySessionStorage = exports.FileSessionStorage = exports.RouterGroup = exports.Response = exports.Request = exports.createApp = void 0;
4
4
  var app_1 = require("./app");
5
5
  Object.defineProperty(exports, "createApp", { enumerable: true, get: function () { return app_1.createApp; } });
6
6
  var http_1 = require("./http");
@@ -17,3 +17,6 @@ var uploader_1 = require("./storage/uploader");
17
17
  Object.defineProperty(exports, "createUploader", { enumerable: true, get: function () { return uploader_1.createUploader; } });
18
18
  var types_1 = require("./storage/types");
19
19
  Object.defineProperty(exports, "StorageType", { enumerable: true, get: function () { return types_1.StorageType; } });
20
+ var validationSchema_1 = require("./validators/validationSchema");
21
+ Object.defineProperty(exports, "v", { enumerable: true, get: function () { return validationSchema_1.v; } });
22
+ Object.defineProperty(exports, "schema", { enumerable: true, get: function () { return validationSchema_1.schema; } });
@@ -75,7 +75,7 @@ const cors = (options = {}) => {
75
75
  corsHeaders["Access-Control-Max-Age"] = String(config.maxAge);
76
76
  if (!config.preflightContinue)
77
77
  return new http_1.Response()
78
- .setStatus(204)
78
+ .setStatusCode(204)
79
79
  .setContent(null)
80
80
  .setHeaders(corsHeaders);
81
81
  }
@@ -6,6 +6,16 @@ import { type MultipartData } from "./parserInterface";
6
6
  * Parses multipart payloads into fields and files.
7
7
  */
8
8
  export declare class MultipartParser implements ContentParser {
9
+ private readonly maxParts;
10
+ private readonly maxFieldSize;
11
+ private readonly maxFileSize;
12
+ private readonly maxHeaderSize;
13
+ constructor(options?: {
14
+ maxParts?: number;
15
+ maxFieldSize?: number;
16
+ maxFileSize?: number;
17
+ maxHeaderSize?: number;
18
+ });
9
19
  /**
10
20
  * Checks whether the given content type is `multipart/form-data`.
11
21
  *
@@ -32,4 +42,5 @@ export declare class MultipartParser implements ContentParser {
32
42
  private splitBuffer;
33
43
  private parsePart;
34
44
  private parseHeaders;
45
+ private trimEndingCRLF;
35
46
  }
@@ -9,6 +9,16 @@ const parserInterface_1 = require("./parserInterface");
9
9
  * Parses multipart payloads into fields and files.
10
10
  */
11
11
  class MultipartParser {
12
+ maxParts;
13
+ maxFieldSize;
14
+ maxFileSize;
15
+ maxHeaderSize;
16
+ constructor(options = {}) {
17
+ this.maxParts = options.maxParts ?? 1000;
18
+ this.maxFieldSize = options.maxFieldSize ?? 1024 * 1024; // 1MB
19
+ this.maxFileSize = options.maxFileSize ?? 10 * 1024 * 1024; // 10MB
20
+ this.maxHeaderSize = options.maxHeaderSize ?? 16 * 1024; // 16KB
21
+ }
12
22
  /**
13
23
  * Checks whether the given content type is `multipart/form-data`.
14
24
  *
@@ -50,24 +60,33 @@ class MultipartParser {
50
60
  };
51
61
  const boundaryBuffer = Buffer.from(`--${boundary}`);
52
62
  const parts = this.splitBuffer(buffer, boundaryBuffer);
63
+ if (parts.length > this.maxParts) {
64
+ throw new httpExceptions_1.UnprocessableContentError(`Multipart parts limit exceeded: ${this.maxParts}`);
65
+ }
53
66
  for (const part of parts) {
54
67
  if (part.length === 0 || part.toString().trim() === "--")
55
68
  continue;
56
69
  const parsed = this.parsePart(part);
57
- if (parsed) {
58
- if (parsed.filename) {
59
- result.files.push({
60
- fieldName: parsed.name,
61
- filename: parsed.filename,
62
- mimetype: parsed.contentType || parserInterface_1.contentTypes["application-octet-stream"],
63
- data: parsed.data,
64
- size: parsed.data.length,
65
- });
66
- }
67
- else {
68
- result.fields[parsed.name] = parsed.data.toString("utf-8");
70
+ if (!parsed)
71
+ continue;
72
+ const size = parsed.data.length;
73
+ if (parsed.filename) {
74
+ if (size > this.maxFileSize) {
75
+ throw new httpExceptions_1.UnprocessableContentError(`File size limit exceeded: ${this.maxFileSize} bytes`);
69
76
  }
77
+ result.files.push({
78
+ fieldName: parsed.name,
79
+ filename: parsed.filename,
80
+ mimetype: parsed.contentType ?? parserInterface_1.contentTypes["application-octet-stream"],
81
+ data: parsed.data,
82
+ size,
83
+ });
84
+ continue;
85
+ }
86
+ if (size > this.maxFieldSize) {
87
+ throw new httpExceptions_1.UnprocessableContentError(`Field size limit exceeded: ${this.maxFieldSize} bytes`);
70
88
  }
89
+ result.fields[parsed.name] = parsed.data.toString("utf-8");
71
90
  }
72
91
  return result;
73
92
  }
@@ -87,8 +106,11 @@ class MultipartParser {
87
106
  const headerEndIndex = part.indexOf("\r\n\r\n");
88
107
  if (headerEndIndex === -1)
89
108
  return null;
109
+ if (headerEndIndex > this.maxHeaderSize) {
110
+ throw new httpExceptions_1.UnprocessableContentError(`Multipart headers too large: ${this.maxHeaderSize} bytes`);
111
+ }
90
112
  const headerSection = part.subarray(0, headerEndIndex).toString("utf-8");
91
- const bodySection = part.subarray(headerEndIndex + 4);
113
+ const bodySection = this.trimEndingCRLF(part.subarray(headerEndIndex + 4));
92
114
  const headers = this.parseHeaders(headerSection);
93
115
  const disposition = headers["content-disposition"];
94
116
  if (!disposition)
@@ -117,5 +139,13 @@ class MultipartParser {
117
139
  }
118
140
  return headers;
119
141
  }
142
+ trimEndingCRLF(data) {
143
+ if (data.length < 2)
144
+ return data;
145
+ const end = data.subarray(-2).toString("utf-8");
146
+ if (end === "\r\n")
147
+ return data.subarray(0, data.length - 2);
148
+ return data;
149
+ }
120
150
  }
121
151
  exports.MultipartParser = MultipartParser;
@@ -73,14 +73,22 @@ class Router {
73
73
  * const response = await router.resolve(request);
74
74
  */
75
75
  resolve(request) {
76
- const layer = this.resolveLayer(request);
77
- if (layer.hasParameters())
78
- request.setParams(layer.parseParameters(request.url));
79
- const action = layer.getAction;
80
- const allMiddlewares = [...this.globalMiddlewares, ...layer.getMiddlewares];
81
- if (allMiddlewares.length > 0)
82
- return this.runMiddlewares(request, allMiddlewares, action);
83
- return action(request);
76
+ const executeLayer = (req) => {
77
+ const layer = this.resolveLayer(req);
78
+ if (layer.hasParameters()) {
79
+ req.setParams(layer.parseParameters(req.url));
80
+ }
81
+ const action = layer.getAction;
82
+ const routeMiddlewares = layer.getMiddlewares;
83
+ if (routeMiddlewares.length > 0) {
84
+ return this.runMiddlewares(req, routeMiddlewares, action);
85
+ }
86
+ return action(req);
87
+ };
88
+ if (this.globalMiddlewares.length > 0) {
89
+ return this.runMiddlewares(request, this.globalMiddlewares, executeLayer);
90
+ }
91
+ return executeLayer(request);
84
92
  }
85
93
  /**
86
94
  * Runs a middleware chain using the onion model.
@@ -213,10 +213,7 @@ class FileSessionStorage {
213
213
  }
214
214
  catch {
215
215
  // Corrupt/unreadable → best-effort cleanup
216
- try {
217
- await (0, promises_1.unlink)(full);
218
- }
219
- catch { }
216
+ await (0, promises_1.unlink)(full);
220
217
  }
221
218
  }));
222
219
  }
@@ -280,7 +277,9 @@ class FileSessionStorage {
280
277
  try {
281
278
  await (0, promises_1.unlink)(path);
282
279
  }
283
- catch { }
280
+ catch {
281
+ /** eslint-disable-next-line no-empty */
282
+ }
284
283
  }
285
284
  /**
286
285
  * Generates a cryptographically strong session id (64 hex chars).
@@ -67,7 +67,7 @@ export interface SessionStorage {
67
67
  * Removes all associated data and invalidates the session
68
68
  * for subsequent requests.
69
69
  */
70
- destroy(): void;
70
+ destroy(): void | Promise<void>;
71
71
  }
72
72
  /**
73
73
  * Internal representation of persisted session data.
@@ -59,7 +59,12 @@ class ContentDisposition {
59
59
  }
60
60
  sanitizeFilename(filename) {
61
61
  return filename
62
- .replace(/[\x00-\x1F\x7F-\x9F]/g, "")
62
+ .split("")
63
+ .filter(char => {
64
+ const code = char.charCodeAt(0);
65
+ return !(code <= 0x1f || (code >= 0x7f && code <= 0x9f));
66
+ })
67
+ .join("")
63
68
  .replace(/["\r\n]/g, "")
64
69
  .replace(/[/\\]/g, "")
65
70
  .replace(/\s+/g, " ")
@@ -86,7 +86,7 @@ class StaticFileHandler {
86
86
  "last-modified": stats.mtime.toUTCString(),
87
87
  ETag: `"${stats.size}-${stats.mtime.getTime()}"`,
88
88
  })
89
- .setStatus(200);
89
+ .setStatusCode(200);
90
90
  }
91
91
  catch {
92
92
  return null;