@trayio/express 4.91.0 → 4.93.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.
@@ -1 +1 @@
1
- {"version":3,"file":"ExpressHttpController.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AASnD,OAAO,EACN,cAAc,EAEd,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAW1D,qBAAa,qBAAqB;IAEhC,SAAS,CAAC,UAAU,EAAE,cAAc;IACpC,SAAS,CAAC,2BAA2B,EAAE,MAAM;IAC7C,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;gBAFzB,UAAU,EAAE,cAAc,EAC1B,2BAA2B,EAAE,MAAe,EAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAGpC,OAAO,CAAC,QAAQ,CA4Ed;IAEF,SAAS,WAAY,MAAM,YAKzB;IAEF,OAAO,CAAC,gBAAgB,CAoBtB;IAGF,OAAO,CAAC,uBAAuB,CAI7B;IAEF,OAAO,CAAC,sBAAsB,CAmC3B;IAEH,OAAO,CAAC,aAAa,CAenB;IAEF,OAAO,CAAC,YAAY,CAkBlB;IAEF,OAAO,CAAC,+BAA+B,CAWpC;CACH"}
1
+ {"version":3,"file":"ExpressHttpController.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AASnD,OAAO,EACN,cAAc,EAEd,MAAM,qCAAqC,CAAC;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,iCAAiC,CAAC;AAW1D,qBAAa,qBAAqB;IAEhC,SAAS,CAAC,UAAU,EAAE,cAAc;IACpC,SAAS,CAAC,2BAA2B,EAAE,MAAM;IAC7C,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;gBAFzB,UAAU,EAAE,cAAc,EAC1B,2BAA2B,EAAE,MAAe,EAC5C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;IAGpC,OAAO,CAAC,QAAQ,CAiGd;IAEF,SAAS,WAAY,MAAM,YAKzB;IAEF,OAAO,CAAC,gBAAgB,CAoBtB;IAGF,OAAO,CAAC,uBAAuB,CAI7B;IAEF,OAAO,CAAC,sBAAsB,CAmC3B;IAEH,OAAO,CAAC,aAAa,CAenB;IAEF,OAAO,CAAC,YAAY,CAkBlB;IAEF,OAAO,CAAC,+BAA+B,CAWpC;CACH"}
@@ -78,8 +78,23 @@ class ExpressHttpController {
78
78
  queryString: req.query,
79
79
  body: requestBody,
80
80
  }))();
81
+ const sanitizeHeaderValue = (value) => {
82
+ let processedValue = value;
83
+ if (Array.isArray(value)) {
84
+ processedValue = value.join(','); // flatten arrays
85
+ }
86
+ // Convert to string and sanitize for HTTP header compliance
87
+ // Remove control characters (0x00-0x1F, 0x7F), line breaks, and other invalid chars
88
+ return (String(processedValue)
89
+ .replace(/\r\n/g, '_') // CRLF line breaks first
90
+ .replace(/[\r\n]/g, '_') // remaining CR or LF
91
+ // eslint-disable-next-line no-control-regex
92
+ .replace(/[\u0000-\u0009\u000B\u000C\u000E-\u001F\u007F]/g, '_') // other control characters (excluding \n and \r)
93
+ .replace(/["\\]/g, '_') // quotes and backslashes
94
+ .trim()); // remove leading/trailing whitespace
95
+ };
81
96
  const response = Object.entries(httpResponse.headers)
82
- .reduce((acc, [key, value]) => acc.setHeader(key, value), res)
97
+ .reduce((acc, [key, value]) => acc.setHeader(key, sanitizeHeaderValue(value)), res)
83
98
  .status(httpResponse.statusCode);
84
99
  httpResponse.body.pipe(response);
85
100
  };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ExpressHttpController.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpressHttpController.unit.test.d.ts","sourceRoot":"","sources":["../../src/http/ExpressHttpController.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const Http_1 = require("@trayio/commons/http/Http");
27
+ const O = __importStar(require("fp-ts/Option"));
28
+ const T = __importStar(require("fp-ts/Task"));
29
+ const stream_1 = require("stream");
30
+ const ExpressHttpController_1 = require("./ExpressHttpController");
31
+ describe('ExpressHttpController', () => {
32
+ let mockHttpController;
33
+ let mockEndpoint;
34
+ let expressHttpController;
35
+ let mockRouter;
36
+ beforeEach(() => {
37
+ mockEndpoint = {
38
+ path: '/test',
39
+ method: Http_1.HttpMethod.Get,
40
+ execute: jest.fn(),
41
+ };
42
+ mockHttpController = {
43
+ getEndpoints: jest.fn().mockReturnValue([mockEndpoint]),
44
+ };
45
+ expressHttpController = new ExpressHttpController_1.ExpressHttpController(mockHttpController, '/tmp', O.none);
46
+ mockRouter = {
47
+ get: jest.fn(),
48
+ post: jest.fn(),
49
+ put: jest.fn(),
50
+ delete: jest.fn(),
51
+ patch: jest.fn(),
52
+ };
53
+ });
54
+ describe('addRoutes', () => {
55
+ it('should add GET route to router', () => {
56
+ expressHttpController.addRoutes(mockRouter);
57
+ expect(mockRouter.get).toHaveBeenCalledWith('/test', expect.any(Function));
58
+ });
59
+ it('should add POST route to router', () => {
60
+ mockEndpoint.method = Http_1.HttpMethod.Post;
61
+ expressHttpController.addRoutes(mockRouter);
62
+ expect(mockRouter.post).toHaveBeenCalledWith('/test', expect.any(Function));
63
+ });
64
+ it('should add PUT route to router', () => {
65
+ mockEndpoint.method = Http_1.HttpMethod.Put;
66
+ expressHttpController.addRoutes(mockRouter);
67
+ expect(mockRouter.put).toHaveBeenCalledWith('/test', expect.any(Function));
68
+ });
69
+ it('should add DELETE route to router', () => {
70
+ mockEndpoint.method = Http_1.HttpMethod.Delete;
71
+ expressHttpController.addRoutes(mockRouter);
72
+ expect(mockRouter.delete).toHaveBeenCalledWith('/test', expect.any(Function));
73
+ });
74
+ it('should add PATCH route to router', () => {
75
+ mockEndpoint.method = Http_1.HttpMethod.Patch;
76
+ expressHttpController.addRoutes(mockRouter);
77
+ expect(mockRouter.patch).toHaveBeenCalledWith('/test', expect.any(Function));
78
+ });
79
+ });
80
+ describe('header sanitization', () => {
81
+ let mockReq;
82
+ let mockRes;
83
+ let routeHandler;
84
+ beforeEach(() => {
85
+ mockReq = {
86
+ headers: {},
87
+ params: {},
88
+ query: {},
89
+ body: {},
90
+ path: '/test',
91
+ };
92
+ mockRes = {
93
+ setHeader: jest.fn().mockReturnThis(),
94
+ status: jest.fn().mockReturnThis(),
95
+ on: jest.fn(),
96
+ once: jest.fn(),
97
+ end: jest.fn(),
98
+ write: jest.fn(),
99
+ removeListener: jest.fn(),
100
+ emit: jest.fn(),
101
+ };
102
+ const mockResponse = new stream_1.Readable();
103
+ mockResponse.push(null);
104
+ const httpResponse = {
105
+ statusCode: 200,
106
+ headers: {},
107
+ body: mockResponse,
108
+ };
109
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
110
+ expressHttpController.addRoutes(mockRouter);
111
+ // eslint-disable-next-line prefer-destructuring
112
+ routeHandler = mockRouter.get.mock.calls[0][1];
113
+ });
114
+ it('should sanitize header values with control characters', async () => {
115
+ const httpResponse = {
116
+ statusCode: 200,
117
+ headers: {
118
+ 'test-header': 'value\x00with\x1fcontrol\x7fchars',
119
+ },
120
+ body: new stream_1.Readable({
121
+ read() {
122
+ this.push(null);
123
+ },
124
+ }),
125
+ };
126
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
127
+ await routeHandler(mockReq, mockRes);
128
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_control_chars');
129
+ });
130
+ it('should sanitize header values with line breaks', async () => {
131
+ const httpResponse = {
132
+ statusCode: 200,
133
+ headers: {
134
+ 'test-header': 'value\nwith\rline\r\nbreaks',
135
+ },
136
+ body: new stream_1.Readable({
137
+ read() {
138
+ this.push(null);
139
+ },
140
+ }),
141
+ };
142
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
143
+ await routeHandler(mockReq, mockRes);
144
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_line_breaks');
145
+ });
146
+ it('should sanitize header values with quotes and backslashes', async () => {
147
+ const httpResponse = {
148
+ statusCode: 200,
149
+ headers: {
150
+ 'test-header': 'value"with\\quotes',
151
+ },
152
+ body: new stream_1.Readable({
153
+ read() {
154
+ this.push(null);
155
+ },
156
+ }),
157
+ };
158
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
159
+ await routeHandler(mockReq, mockRes);
160
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with_quotes');
161
+ });
162
+ it('should trim whitespace from header values', async () => {
163
+ const httpResponse = {
164
+ statusCode: 200,
165
+ headers: {
166
+ 'test-header': ' value with spaces ',
167
+ },
168
+ body: new stream_1.Readable({
169
+ read() {
170
+ this.push(null);
171
+ },
172
+ }),
173
+ };
174
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
175
+ await routeHandler(mockReq, mockRes);
176
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value with spaces');
177
+ });
178
+ it('should handle array header values by joining with comma', async () => {
179
+ const httpResponse = {
180
+ statusCode: 200,
181
+ headers: {
182
+ 'test-header': ['value1', 'value2', 'value3'],
183
+ },
184
+ body: new stream_1.Readable({
185
+ read() {
186
+ this.push(null);
187
+ },
188
+ }),
189
+ };
190
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
191
+ await routeHandler(mockReq, mockRes);
192
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value1,value2,value3');
193
+ });
194
+ it('should handle numeric header values', async () => {
195
+ const httpResponse = {
196
+ statusCode: 200,
197
+ headers: {
198
+ 'content-length': '123',
199
+ },
200
+ body: new stream_1.Readable({
201
+ read() {
202
+ this.push(null);
203
+ },
204
+ }),
205
+ };
206
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
207
+ await routeHandler(mockReq, mockRes);
208
+ expect(mockRes.setHeader).toHaveBeenCalledWith('content-length', '123');
209
+ });
210
+ it('should sanitize complex header values with multiple issues', async () => {
211
+ const httpResponse = {
212
+ statusCode: 200,
213
+ headers: {
214
+ 'test-header': ' value\x00with\r\n"multiple\\issues" ',
215
+ },
216
+ body: new stream_1.Readable({
217
+ read() {
218
+ this.push(null);
219
+ },
220
+ }),
221
+ };
222
+ mockEndpoint.execute.mockReturnValue(() => T.of(httpResponse));
223
+ await routeHandler(mockReq, mockRes);
224
+ expect(mockRes.setHeader).toHaveBeenCalledWith('test-header', 'value_with__multiple_issues_');
225
+ });
226
+ });
227
+ describe('error handling', () => {
228
+ let mockReq;
229
+ let mockRes;
230
+ let routeHandler;
231
+ beforeEach(() => {
232
+ mockReq = {
233
+ headers: { 'content-type': 'multipart/form-data' },
234
+ params: {},
235
+ query: {},
236
+ body: {},
237
+ path: '/test',
238
+ };
239
+ mockRes = {
240
+ setHeader: jest.fn().mockReturnThis(),
241
+ status: jest.fn().mockReturnThis(),
242
+ json: jest.fn().mockReturnThis(),
243
+ };
244
+ expressHttpController = new ExpressHttpController_1.ExpressHttpController(mockHttpController, '/tmp', O.some({
245
+ warning: jest.fn(),
246
+ }));
247
+ expressHttpController.addRoutes(mockRouter);
248
+ // eslint-disable-next-line prefer-destructuring
249
+ routeHandler = mockRouter.get.mock.calls[0][1];
250
+ });
251
+ it('should handle file too large error', async () => {
252
+ jest
253
+ .spyOn(expressHttpController, 'parseRequestBody')
254
+ .mockRejectedValue(new Error('maxFileSize exceeded'));
255
+ await routeHandler(mockReq, mockRes);
256
+ expect(mockRes.status).toHaveBeenCalledWith(400);
257
+ expect(mockRes.json).toHaveBeenCalledWith({
258
+ error: 'File too large. Max size is 100MB.',
259
+ });
260
+ });
261
+ it('should handle general parsing errors', async () => {
262
+ jest
263
+ .spyOn(expressHttpController, 'parseRequestBody')
264
+ .mockRejectedValue(new Error('Invalid format'));
265
+ await routeHandler(mockReq, mockRes);
266
+ expect(mockRes.status).toHaveBeenCalledWith(400);
267
+ expect(mockRes.json).toHaveBeenCalledWith({
268
+ error: 'Invalid request: Invalid format',
269
+ });
270
+ });
271
+ });
272
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trayio/express",
3
- "version": "4.91.0",
3
+ "version": "4.93.0",
4
4
  "description": "Express extensions and implementations",
5
5
  "exports": {
6
6
  "./*": "./dist/*.js"
@@ -14,7 +14,7 @@
14
14
  "access": "public"
15
15
  },
16
16
  "dependencies": {
17
- "@trayio/commons": "4.91.0",
17
+ "@trayio/commons": "4.93.0",
18
18
  "cors": "2.8.5",
19
19
  "express": "4.20.0",
20
20
  "formidable": "3.5.1"