te.js 2.1.6 → 2.2.1

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 (55) hide show
  1. package/README.md +1 -12
  2. package/auto-docs/analysis/handler-analyzer.test.js +106 -0
  3. package/auto-docs/analysis/source-resolver.test.js +58 -0
  4. package/auto-docs/constants.js +13 -2
  5. package/auto-docs/openapi/generator.js +7 -5
  6. package/auto-docs/openapi/generator.test.js +132 -0
  7. package/auto-docs/openapi/spec-builders.js +39 -19
  8. package/cli/docs-command.js +44 -36
  9. package/cors/index.test.js +82 -0
  10. package/docs/README.md +1 -2
  11. package/docs/api-reference.md +124 -186
  12. package/docs/configuration.md +0 -13
  13. package/docs/getting-started.md +19 -21
  14. package/docs/rate-limiting.md +59 -58
  15. package/lib/llm/client.js +7 -2
  16. package/lib/llm/index.js +14 -1
  17. package/lib/llm/parse.test.js +60 -0
  18. package/package.json +3 -1
  19. package/radar/index.js +382 -0
  20. package/rate-limit/base.js +12 -15
  21. package/rate-limit/index.js +19 -22
  22. package/rate-limit/index.test.js +93 -0
  23. package/rate-limit/storage/memory.js +13 -13
  24. package/rate-limit/storage/redis-install.js +70 -0
  25. package/rate-limit/storage/redis.js +94 -52
  26. package/server/ammo/body-parser.js +156 -152
  27. package/server/ammo/body-parser.test.js +79 -0
  28. package/server/ammo/enhancer.js +8 -4
  29. package/server/ammo.js +138 -12
  30. package/server/context/request-context.js +51 -0
  31. package/server/context/request-context.test.js +53 -0
  32. package/server/endpoint.js +15 -0
  33. package/server/error.js +56 -3
  34. package/server/error.test.js +45 -0
  35. package/server/errors/channels/channels.test.js +148 -0
  36. package/server/errors/channels/index.js +1 -1
  37. package/server/errors/llm-cache.js +1 -1
  38. package/server/errors/llm-cache.test.js +160 -0
  39. package/server/errors/llm-error-service.js +1 -1
  40. package/server/errors/llm-rate-limiter.test.js +105 -0
  41. package/server/files/uploader.js +38 -26
  42. package/server/handler.js +1 -1
  43. package/server/targets/registry.js +3 -3
  44. package/server/targets/registry.test.js +108 -0
  45. package/te.js +233 -183
  46. package/utils/auto-register.js +1 -1
  47. package/utils/configuration.js +23 -9
  48. package/utils/configuration.test.js +58 -0
  49. package/utils/errors-llm-config.js +74 -8
  50. package/utils/request-logger.js +49 -3
  51. package/utils/startup.js +80 -0
  52. package/database/index.js +0 -165
  53. package/database/mongodb.js +0 -146
  54. package/database/redis.js +0 -201
  55. package/docs/database.md +0 -390
@@ -28,190 +28,194 @@ async function parseDataBasedOnContentType(req) {
28
28
  }
29
29
 
30
30
  function parseJSONRequestBody(req) {
31
- return new Promise((resolve, reject) => {
32
- let body = '';
33
- let size = 0;
34
- const maxSize = env('BODY_MAX_SIZE');
35
- const timeout = setTimeout(() => {
36
- reject(new BodyParserError('Request timeout', 408));
37
- }, env('BODY_TIMEOUT'));
38
-
39
- req.on('data', (chunk) => {
40
- size += chunk.length;
41
- if (size > maxSize) {
42
- clearTimeout(timeout);
43
- reject(new BodyParserError('Request entity too large', 413));
44
- req.destroy();
45
- return;
46
- }
47
- body += chunk.toString();
48
- });
49
-
50
- req.on('error', (err) => {
31
+ const { promise, resolve, reject } = Promise.withResolvers();
32
+ let body = '';
33
+ let size = 0;
34
+ const maxSize = env('BODY_MAX_SIZE');
35
+ const timeout = setTimeout(() => {
36
+ reject(new BodyParserError('Request timeout', 408));
37
+ }, env('BODY_TIMEOUT'));
38
+
39
+ req.on('data', (chunk) => {
40
+ size += chunk.length;
41
+ if (size > maxSize) {
51
42
  clearTimeout(timeout);
52
- reject(new BodyParserError(`Request error: ${err.message}`, 400));
53
- });
43
+ reject(new BodyParserError('Request entity too large', 413));
44
+ req.destroy();
45
+ return;
46
+ }
47
+ body += chunk.toString();
48
+ });
54
49
 
55
- req.on('end', () => {
56
- clearTimeout(timeout);
57
- try {
58
- if (!body) {
59
- resolve({});
60
- return;
61
- }
62
- const jsonData = JSON.parse(body);
63
- if (typeof jsonData !== 'object') {
64
- throw new TejError(400, 'Invalid JSON structure');
65
- }
66
- resolve(jsonData);
67
- } catch (err) {
68
- reject(new TejError(400, `Invalid JSON: ${err.message}`));
69
- }
70
- });
50
+ req.on('error', (err) => {
51
+ clearTimeout(timeout);
52
+ reject(new BodyParserError(`Request error: ${err.message}`, 400));
71
53
  });
72
- }
73
54
 
74
- function parseUrlEncodedData(req) {
75
- return new Promise((resolve, reject) => {
76
- let body = '';
77
- let size = 0;
78
- const maxSize = env('BODY_MAX_SIZE');
79
- const timeout = setTimeout(() => {
80
- reject(new BodyParserError('Request timeout', 408));
81
- }, env('BODY_TIMEOUT'));
82
-
83
- req.on('data', (chunk) => {
84
- size += chunk.length;
85
- if (size > maxSize) {
86
- clearTimeout(timeout);
87
- reject(new BodyParserError('Request entity too large', 413));
88
- req.destroy();
55
+ req.on('end', () => {
56
+ clearTimeout(timeout);
57
+ try {
58
+ if (!body) {
59
+ resolve({});
89
60
  return;
90
61
  }
91
- body += chunk.toString();
92
- });
62
+ const jsonData = JSON.parse(body);
63
+ if (typeof jsonData !== 'object') {
64
+ throw new TejError(400, 'Invalid JSON structure');
65
+ }
66
+ resolve(jsonData);
67
+ } catch (err) {
68
+ reject(new TejError(400, `Invalid JSON: ${err.message}`));
69
+ }
70
+ });
93
71
 
94
- req.on('error', (err) => {
95
- clearTimeout(timeout);
96
- reject(new BodyParserError(`Request error: ${err.message}`, 400));
97
- });
72
+ return promise;
73
+ }
98
74
 
99
- req.on('end', () => {
75
+ function parseUrlEncodedData(req) {
76
+ const { promise, resolve, reject } = Promise.withResolvers();
77
+ let body = '';
78
+ let size = 0;
79
+ const maxSize = env('BODY_MAX_SIZE');
80
+ const timeout = setTimeout(() => {
81
+ reject(new BodyParserError('Request timeout', 408));
82
+ }, env('BODY_TIMEOUT'));
83
+
84
+ req.on('data', (chunk) => {
85
+ size += chunk.length;
86
+ if (size > maxSize) {
100
87
  clearTimeout(timeout);
101
- try {
102
- if (!body) {
103
- resolve({});
104
- return;
105
- }
106
- const data = new URLSearchParams(body);
107
- const parsedData = Object.fromEntries(data);
108
- resolve(parsedData);
109
- } catch (err) {
110
- reject(new BodyParserError('Invalid URL encoded data', 400));
111
- }
112
- });
88
+ reject(new BodyParserError('Request entity too large', 413));
89
+ req.destroy();
90
+ return;
91
+ }
92
+ body += chunk.toString();
113
93
  });
114
- }
115
94
 
116
- function parseFormData(req) {
117
- return new Promise((resolve, reject) => {
118
- let body = '';
119
- let size = 0;
120
- const maxSize = env('BODY_MAX_SIZE');
121
- const timeout = setTimeout(() => {
122
- reject(new BodyParserError('Request timeout', 408));
123
- }, env('BODY_TIMEOUT'));
124
-
125
- req.on('data', (chunk) => {
126
- size += chunk.length;
127
- if (size > maxSize) {
128
- clearTimeout(timeout);
129
- reject(new BodyParserError('Request entity too large', 413));
130
- req.destroy();
95
+ req.on('error', (err) => {
96
+ clearTimeout(timeout);
97
+ reject(new BodyParserError(`Request error: ${err.message}`, 400));
98
+ });
99
+
100
+ req.on('end', () => {
101
+ clearTimeout(timeout);
102
+ try {
103
+ if (!body) {
104
+ resolve({});
131
105
  return;
132
106
  }
133
- body += chunk.toString();
134
- });
107
+ const data = new URLSearchParams(body);
108
+ const parsedData = Object.fromEntries(data);
109
+ resolve(parsedData);
110
+ } catch (err) {
111
+ reject(new BodyParserError('Invalid URL encoded data', 400));
112
+ }
113
+ });
135
114
 
136
- req.on('error', (err) => {
137
- clearTimeout(timeout);
138
- reject(new BodyParserError(`Request error: ${err.message}`, 400));
139
- });
115
+ return promise;
116
+ }
140
117
 
141
- req.on('end', () => {
118
+ function parseFormData(req) {
119
+ const { promise, resolve, reject } = Promise.withResolvers();
120
+ let body = '';
121
+ let size = 0;
122
+ const maxSize = env('BODY_MAX_SIZE');
123
+ const timeout = setTimeout(() => {
124
+ reject(new BodyParserError('Request timeout', 408));
125
+ }, env('BODY_TIMEOUT'));
126
+
127
+ req.on('data', (chunk) => {
128
+ size += chunk.length;
129
+ if (size > maxSize) {
142
130
  clearTimeout(timeout);
143
- try {
144
- if (!body.trim()) {
145
- resolve([]);
146
- return;
147
- }
131
+ reject(new BodyParserError('Request entity too large', 413));
132
+ req.destroy();
133
+ return;
134
+ }
135
+ body += chunk.toString();
136
+ });
148
137
 
149
- const contentType = req.headers['content-type'];
150
- const boundaryMatch = contentType.match(
151
- /boundary=(?:"([^"]+)"|([^;]+))/i,
152
- );
138
+ req.on('error', (err) => {
139
+ clearTimeout(timeout);
140
+ reject(new BodyParserError(`Request error: ${err.message}`, 400));
141
+ });
153
142
 
154
- if (!boundaryMatch) {
155
- throw new TejError(400, 'Missing boundary in content-type');
156
- }
143
+ req.on('end', () => {
144
+ clearTimeout(timeout);
145
+ try {
146
+ if (!body.trim()) {
147
+ resolve([]);
148
+ return;
149
+ }
157
150
 
158
- const boundary = '--' + (boundaryMatch[1] || boundaryMatch[2]);
159
- const parts = body
160
- .split(boundary)
161
- .filter((part) => part.trim() !== '' && part.trim() !== '--');
151
+ const contentType = req.headers['content-type'];
152
+ const boundaryMatch = contentType.match(
153
+ /boundary=(?:"([^"]+)"|([^;]+))/i,
154
+ );
162
155
 
163
- const parsedData = parts.map((part) => {
164
- const [headerString, ...contentParts] = part.split('\r\n\r\n');
165
- if (!headerString || contentParts.length === 0) {
166
- throw new TejError(400, 'Malformed multipart part');
167
- }
156
+ if (!boundaryMatch) {
157
+ throw new TejError(400, 'Missing boundary in content-type');
158
+ }
168
159
 
169
- const headers = {};
170
- const headerLines = headerString.trim().split('\r\n');
160
+ const boundary = '--' + (boundaryMatch[1] || boundaryMatch[2]);
161
+ const parts = body
162
+ .split(boundary)
163
+ .filter((part) => part.trim() !== '' && part.trim() !== '--');
171
164
 
172
- headerLines.forEach((line) => {
173
- const [key, ...valueParts] = line.split(': ');
174
- if (!key || valueParts.length === 0) {
175
- throw new TejError(400, 'Malformed header');
176
- }
177
- headers[key.toLowerCase()] = valueParts.join(': ');
178
- });
165
+ const parsedData = parts.map((part) => {
166
+ const [headerString, ...contentParts] = part.split('\r\n\r\n');
167
+ if (!headerString || contentParts.length === 0) {
168
+ throw new TejError(400, 'Malformed multipart part');
169
+ }
179
170
 
180
- const value = contentParts.join('\r\n\r\n').replace(/\r\n$/, '');
171
+ const headers = Object.create(null);
172
+ const headerLines = headerString.trim().split('\r\n');
181
173
 
182
- // Parse content-disposition
183
- const disposition = headers['content-disposition'];
184
- if (!disposition) {
185
- throw new TejError(400, 'Missing content-disposition header');
174
+ headerLines.forEach((line) => {
175
+ const [key, ...valueParts] = line.split(': ');
176
+ if (!key || valueParts.length === 0) {
177
+ throw new TejError(400, 'Malformed header');
186
178
  }
179
+ headers[key.toLowerCase()] = valueParts.join(': ');
180
+ });
187
181
 
188
- const nameMatch = disposition.match(/name="([^"]+)"/);
189
- const filename = disposition.match(/filename="([^"]+)"/);
182
+ const value = contentParts.join('\r\n\r\n').replace(/\r\n$/, '');
190
183
 
191
- return {
192
- name: nameMatch ? nameMatch[1] : undefined,
193
- filename: filename ? filename[1] : undefined,
194
- headers,
195
- value,
196
- };
197
- });
184
+ const disposition = headers['content-disposition'];
185
+ if (!disposition) {
186
+ throw new TejError(400, 'Missing content-disposition header');
187
+ }
198
188
 
199
- resolve(parsedData);
200
- } catch (err) {
201
- reject(
202
- new BodyParserError(
203
- `Invalid multipart form data: ${err.message}`,
204
- 400,
205
- ),
206
- );
207
- }
208
- });
189
+ const nameMatch = disposition.match(/name="([^"]+)"/);
190
+ const filename = disposition.match(/filename="([^"]+)"/);
191
+
192
+ return {
193
+ name: nameMatch ? nameMatch[1] : undefined,
194
+ filename: filename ? filename[1] : undefined,
195
+ headers,
196
+ value,
197
+ };
198
+ });
199
+
200
+ resolve(parsedData);
201
+ } catch (err) {
202
+ reject(
203
+ new BodyParserError(`Invalid multipart form data: ${err.message}`, 400),
204
+ );
205
+ }
209
206
  });
207
+
208
+ return promise;
210
209
  }
211
210
 
212
211
  class BodyParserError extends TejError {
213
- constructor(message, statusCode = 400) {
214
- super(statusCode, message);
212
+ /**
213
+ * @param {string} message - Human-readable description
214
+ * @param {number} [statusCode=400] - HTTP status code
215
+ * @param {{ cause?: Error }} [options] - Optional cause for chaining
216
+ */
217
+ constructor(message, statusCode = 400, options) {
218
+ super(statusCode, message, options);
215
219
  this.name = 'BodyParserError';
216
220
  }
217
221
  }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @fileoverview Tests for body-parser (JSON, URL-encoded, multipart).
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import parseDataBasedOnContentType, { BodyParserError } from './body-parser.js';
6
+ import { MockRequest } from '../../tests/helpers/mock-http.js';
7
+
8
+ function makeReq(contentType, body) {
9
+ const req = new MockRequest({
10
+ method: 'POST',
11
+ headers: contentType ? { 'content-type': contentType } : {},
12
+ });
13
+ // Schedule body emission after current tick
14
+ setImmediate(() => req.simulateBody(body || ''));
15
+ return req;
16
+ }
17
+
18
+ describe('parseDataBasedOnContentType', () => {
19
+ it('should return empty object when no content-type', async () => {
20
+ const req = new MockRequest({ method: 'POST', headers: {} });
21
+ const result = await parseDataBasedOnContentType(req);
22
+ expect(result).toEqual({});
23
+ });
24
+
25
+ it('should parse JSON body', async () => {
26
+ const req = makeReq('application/json', JSON.stringify({ name: 'Tejas' }));
27
+ const result = await parseDataBasedOnContentType(req);
28
+ expect(result).toEqual({ name: 'Tejas' });
29
+ });
30
+
31
+ it('should return empty object for empty JSON body', async () => {
32
+ const req = makeReq('application/json', '');
33
+ const result = await parseDataBasedOnContentType(req);
34
+ expect(result).toEqual({});
35
+ });
36
+
37
+ it('should reject on invalid JSON', async () => {
38
+ const req = makeReq('application/json', '{not valid json}');
39
+ await expect(parseDataBasedOnContentType(req)).rejects.toBeInstanceOf(
40
+ Error,
41
+ );
42
+ });
43
+
44
+ it('should parse URL-encoded body', async () => {
45
+ const req = makeReq(
46
+ 'application/x-www-form-urlencoded',
47
+ 'name=Alice&age=30',
48
+ );
49
+ const result = await parseDataBasedOnContentType(req);
50
+ expect(result.name).toBe('Alice');
51
+ expect(result.age).toBe('30');
52
+ });
53
+
54
+ it('should throw BodyParserError for unsupported content type', async () => {
55
+ const req = new MockRequest({
56
+ method: 'POST',
57
+ headers: { 'content-type': 'text/xml' },
58
+ });
59
+ setImmediate(() => req.simulateBody('<xml/>'));
60
+ await expect(parseDataBasedOnContentType(req)).rejects.toBeInstanceOf(
61
+ BodyParserError,
62
+ );
63
+ });
64
+ });
65
+
66
+ describe('BodyParserError', () => {
67
+ it('should extend TejError', async () => {
68
+ const TejError = (await import('../error.js')).default;
69
+ const err = new BodyParserError('Bad body', 400);
70
+ expect(err).toBeInstanceOf(TejError);
71
+ expect(err.statusCode).toBe(400);
72
+ expect(err.name).toBe('BodyParserError');
73
+ });
74
+
75
+ it('should default statusCode to 400', () => {
76
+ const err = new BodyParserError('Bad body');
77
+ expect(err.statusCode).toBe(400);
78
+ });
79
+ });
@@ -13,19 +13,23 @@ function hostname(req) {
13
13
  }
14
14
 
15
15
  async function generatePayload(req) {
16
- const obj = {};
16
+ const obj = Object.create(null);
17
17
 
18
- const searchParams = new URLSearchParams(req.url.split('?')[1]);
18
+ const searchParams = new URLSearchParams((req.url ?? '').split('?')[1] ?? '');
19
19
  for (const [key, value] of searchParams) {
20
20
  obj[key] = value;
21
21
  }
22
22
 
23
23
  // Only parse body for methods that typically have a body
24
- if (req.method !== 'GET' && req.method !== 'HEAD' && req.method !== 'DELETE') {
24
+ if (
25
+ req.method !== 'GET' &&
26
+ req.method !== 'HEAD' &&
27
+ req.method !== 'DELETE'
28
+ ) {
25
29
  const body = await bodyParser(req);
26
30
  if (body) Object.assign(obj, body);
27
31
  }
28
-
32
+
29
33
  return obj;
30
34
  }
31
35