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.
- package/README.md +1 -12
- package/auto-docs/analysis/handler-analyzer.test.js +106 -0
- package/auto-docs/analysis/source-resolver.test.js +58 -0
- package/auto-docs/constants.js +13 -2
- package/auto-docs/openapi/generator.js +7 -5
- package/auto-docs/openapi/generator.test.js +132 -0
- package/auto-docs/openapi/spec-builders.js +39 -19
- package/cli/docs-command.js +44 -36
- package/cors/index.test.js +82 -0
- package/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +7 -2
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +382 -0
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +19 -22
- package/rate-limit/index.test.js +93 -0
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- package/server/ammo/body-parser.js +156 -152
- package/server/ammo/body-parser.test.js +79 -0
- package/server/ammo/enhancer.js +8 -4
- package/server/ammo.js +138 -12
- package/server/context/request-context.js +51 -0
- package/server/context/request-context.test.js +53 -0
- package/server/endpoint.js +15 -0
- package/server/error.js +56 -3
- package/server/error.test.js +45 -0
- package/server/errors/channels/channels.test.js +148 -0
- package/server/errors/channels/index.js +1 -1
- package/server/errors/llm-cache.js +1 -1
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +1 -1
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +1 -1
- package/server/targets/registry.js +3 -3
- package/server/targets/registry.test.js +108 -0
- package/te.js +233 -183
- package/utils/auto-register.js +1 -1
- package/utils/configuration.js +23 -9
- package/utils/configuration.test.js +58 -0
- package/utils/errors-llm-config.js +74 -8
- package/utils/request-logger.js +49 -3
- package/utils/startup.js +80 -0
- package/database/index.js +0 -165
- package/database/mongodb.js +0 -146
- package/database/redis.js +0 -201
- package/docs/database.md +0 -390
|
@@ -28,190 +28,194 @@ async function parseDataBasedOnContentType(req) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function parseJSONRequestBody(req) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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(
|
|
53
|
-
|
|
43
|
+
reject(new BodyParserError('Request entity too large', 413));
|
|
44
|
+
req.destroy();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
body += chunk.toString();
|
|
48
|
+
});
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
reject(new BodyParserError(`Request error: ${err.message}`, 400));
|
|
97
|
-
});
|
|
72
|
+
return promise;
|
|
73
|
+
}
|
|
98
74
|
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
reject(new BodyParserError(`Request error: ${err.message}`, 400));
|
|
139
|
-
});
|
|
115
|
+
return promise;
|
|
116
|
+
}
|
|
140
117
|
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
reject(new BodyParserError('Request entity too large', 413));
|
|
132
|
+
req.destroy();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
body += chunk.toString();
|
|
136
|
+
});
|
|
148
137
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
138
|
+
req.on('error', (err) => {
|
|
139
|
+
clearTimeout(timeout);
|
|
140
|
+
reject(new BodyParserError(`Request error: ${err.message}`, 400));
|
|
141
|
+
});
|
|
153
142
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
143
|
+
req.on('end', () => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
try {
|
|
146
|
+
if (!body.trim()) {
|
|
147
|
+
resolve([]);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
157
150
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
151
|
+
const contentType = req.headers['content-type'];
|
|
152
|
+
const boundaryMatch = contentType.match(
|
|
153
|
+
/boundary=(?:"([^"]+)"|([^;]+))/i,
|
|
154
|
+
);
|
|
162
155
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
160
|
+
const boundary = '--' + (boundaryMatch[1] || boundaryMatch[2]);
|
|
161
|
+
const parts = body
|
|
162
|
+
.split(boundary)
|
|
163
|
+
.filter((part) => part.trim() !== '' && part.trim() !== '--');
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
171
|
+
const headers = Object.create(null);
|
|
172
|
+
const headerLines = headerString.trim().split('\r\n');
|
|
181
173
|
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
if (!
|
|
185
|
-
throw new TejError(400, '
|
|
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
|
-
|
|
189
|
-
const filename = disposition.match(/filename="([^"]+)"/);
|
|
182
|
+
const value = contentParts.join('\r\n\r\n').replace(/\r\n$/, '');
|
|
190
183
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
+
});
|
package/server/ammo/enhancer.js
CHANGED
|
@@ -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 (
|
|
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
|
|