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
package/docs/rate-limiting.md
CHANGED
|
@@ -10,10 +10,9 @@ import Tejas from 'te.js';
|
|
|
10
10
|
const app = new Tejas();
|
|
11
11
|
|
|
12
12
|
app
|
|
13
|
-
.withRedis({ url: 'redis://localhost:6379' })
|
|
14
13
|
.withRateLimit({
|
|
15
14
|
maxRequests: 100,
|
|
16
|
-
timeWindowSeconds: 60
|
|
15
|
+
timeWindowSeconds: 60,
|
|
17
16
|
})
|
|
18
17
|
.takeoff();
|
|
19
18
|
```
|
|
@@ -25,35 +24,35 @@ This limits all endpoints to 100 requests per minute per IP address.
|
|
|
25
24
|
```javascript
|
|
26
25
|
app.withRateLimit({
|
|
27
26
|
// Core settings
|
|
28
|
-
maxRequests: 100,
|
|
29
|
-
timeWindowSeconds: 60,
|
|
30
|
-
|
|
27
|
+
maxRequests: 100, // Maximum requests in time window
|
|
28
|
+
timeWindowSeconds: 60, // Time window in seconds
|
|
29
|
+
|
|
31
30
|
// Algorithm selection
|
|
32
31
|
algorithm: 'sliding-window', // 'sliding-window' | 'token-bucket' | 'fixed-window'
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
// Storage backend
|
|
35
|
-
store: '
|
|
36
|
-
|
|
34
|
+
store: 'memory', // 'memory' or { type: 'redis', url: '...' }
|
|
35
|
+
|
|
37
36
|
// Custom key generator (defaults to IP-based)
|
|
38
37
|
keyGenerator: (ammo) => ammo.ip,
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
// Algorithm-specific options
|
|
41
40
|
algorithmOptions: {},
|
|
42
|
-
|
|
41
|
+
|
|
43
42
|
// Key prefix for storage keys (useful for namespacing)
|
|
44
43
|
keyPrefix: 'rl:',
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
// Header format
|
|
47
46
|
headerFormat: {
|
|
48
|
-
type: 'standard',
|
|
49
|
-
draft7: false,
|
|
50
|
-
draft8: false
|
|
47
|
+
type: 'standard', // 'standard' | 'legacy' | 'both'
|
|
48
|
+
draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
|
|
49
|
+
draft8: false, // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
|
|
51
50
|
},
|
|
52
|
-
|
|
51
|
+
|
|
53
52
|
// Custom handler when rate limited
|
|
54
53
|
onRateLimited: (ammo) => {
|
|
55
54
|
ammo.fire(429, { error: 'Slow down!' });
|
|
56
|
-
}
|
|
55
|
+
},
|
|
57
56
|
});
|
|
58
57
|
```
|
|
59
58
|
|
|
@@ -67,7 +66,7 @@ Best for smooth, accurate rate limiting. Prevents the "burst at window boundary"
|
|
|
67
66
|
app.withRateLimit({
|
|
68
67
|
maxRequests: 100,
|
|
69
68
|
timeWindowSeconds: 60,
|
|
70
|
-
algorithm: 'sliding-window'
|
|
69
|
+
algorithm: 'sliding-window',
|
|
71
70
|
});
|
|
72
71
|
```
|
|
73
72
|
|
|
@@ -83,9 +82,9 @@ app.withRateLimit({
|
|
|
83
82
|
timeWindowSeconds: 60,
|
|
84
83
|
algorithm: 'token-bucket',
|
|
85
84
|
algorithmOptions: {
|
|
86
|
-
refillRate: 1.67,
|
|
87
|
-
burstSize: 150
|
|
88
|
-
}
|
|
85
|
+
refillRate: 1.67, // Tokens per second (100/60)
|
|
86
|
+
burstSize: 150, // Maximum tokens (allows 50% burst)
|
|
87
|
+
},
|
|
89
88
|
});
|
|
90
89
|
```
|
|
91
90
|
|
|
@@ -101,8 +100,8 @@ app.withRateLimit({
|
|
|
101
100
|
timeWindowSeconds: 60,
|
|
102
101
|
algorithm: 'fixed-window',
|
|
103
102
|
algorithmOptions: {
|
|
104
|
-
strictWindow: true
|
|
105
|
-
}
|
|
103
|
+
strictWindow: true, // Align to clock boundaries
|
|
104
|
+
},
|
|
106
105
|
});
|
|
107
106
|
```
|
|
108
107
|
|
|
@@ -118,31 +117,34 @@ Good for single-server deployments:
|
|
|
118
117
|
app.withRateLimit({
|
|
119
118
|
maxRequests: 100,
|
|
120
119
|
timeWindowSeconds: 60,
|
|
121
|
-
store: 'memory'
|
|
120
|
+
store: 'memory',
|
|
122
121
|
});
|
|
123
122
|
```
|
|
124
123
|
|
|
125
124
|
**Pros:** No external dependencies, fast
|
|
126
|
-
**Cons:** Not shared between server instances
|
|
125
|
+
**Cons:** Not shared between server instances — may be inaccurate in distributed deployments
|
|
126
|
+
|
|
127
|
+
> **Warning:** If you run multiple server instances (e.g. behind a load balancer), each instance tracks its own counters independently. Use Redis storage below for accurate distributed rate limiting.
|
|
127
128
|
|
|
128
129
|
### Redis
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
For distributed / multi-instance deployments where counters must be shared:
|
|
131
132
|
|
|
132
133
|
```javascript
|
|
133
|
-
app
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
134
|
+
app.withRateLimit({
|
|
135
|
+
maxRequests: 100,
|
|
136
|
+
timeWindowSeconds: 60,
|
|
137
|
+
store: {
|
|
138
|
+
type: 'redis',
|
|
139
|
+
url: 'redis://localhost:6379',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
140
142
|
```
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
**Cons:** Requires Redis server, slightly higher latency
|
|
144
|
+
The `redis` npm package is auto-installed on first use if not already present. Any additional properties in the `store` object are forwarded to the node-redis `createClient` call.
|
|
144
145
|
|
|
145
|
-
|
|
146
|
+
**Pros:** Shared across all server instances, persistent
|
|
147
|
+
**Cons:** Requires a Redis server, slightly higher latency
|
|
146
148
|
|
|
147
149
|
## Custom Key Generation
|
|
148
150
|
|
|
@@ -154,7 +156,7 @@ By default, rate limiting is based on client IP. Customize this:
|
|
|
154
156
|
app.withRateLimit({
|
|
155
157
|
maxRequests: 100,
|
|
156
158
|
timeWindowSeconds: 60,
|
|
157
|
-
keyGenerator: (ammo) => ammo.user?.id || ammo.ip
|
|
159
|
+
keyGenerator: (ammo) => ammo.user?.id || ammo.ip,
|
|
158
160
|
});
|
|
159
161
|
```
|
|
160
162
|
|
|
@@ -164,7 +166,7 @@ app.withRateLimit({
|
|
|
164
166
|
app.withRateLimit({
|
|
165
167
|
maxRequests: 1000,
|
|
166
168
|
timeWindowSeconds: 60,
|
|
167
|
-
keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip
|
|
169
|
+
keyGenerator: (ammo) => ammo.headers['x-api-key'] || ammo.ip,
|
|
168
170
|
});
|
|
169
171
|
```
|
|
170
172
|
|
|
@@ -174,7 +176,7 @@ app.withRateLimit({
|
|
|
174
176
|
app.withRateLimit({
|
|
175
177
|
maxRequests: 100,
|
|
176
178
|
timeWindowSeconds: 60,
|
|
177
|
-
keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}
|
|
179
|
+
keyGenerator: (ammo) => `${ammo.ip}:${ammo.endpoint}`,
|
|
178
180
|
});
|
|
179
181
|
```
|
|
180
182
|
|
|
@@ -194,7 +196,7 @@ RateLimit-Reset: 1706540400
|
|
|
194
196
|
|
|
195
197
|
```javascript
|
|
196
198
|
app.withRateLimit({
|
|
197
|
-
headerFormat: { type: 'legacy' }
|
|
199
|
+
headerFormat: { type: 'legacy' },
|
|
198
200
|
});
|
|
199
201
|
```
|
|
200
202
|
|
|
@@ -208,7 +210,7 @@ X-RateLimit-Reset: 1706540400
|
|
|
208
210
|
|
|
209
211
|
```javascript
|
|
210
212
|
app.withRateLimit({
|
|
211
|
-
headerFormat: { type: 'both' }
|
|
213
|
+
headerFormat: { type: 'both' },
|
|
212
214
|
});
|
|
213
215
|
```
|
|
214
216
|
|
|
@@ -216,7 +218,7 @@ app.withRateLimit({
|
|
|
216
218
|
|
|
217
219
|
```javascript
|
|
218
220
|
app.withRateLimit({
|
|
219
|
-
headerFormat: { type: 'standard', draft7: true }
|
|
221
|
+
headerFormat: { type: 'standard', draft7: true },
|
|
220
222
|
});
|
|
221
223
|
```
|
|
222
224
|
|
|
@@ -238,9 +240,9 @@ app.withRateLimit({
|
|
|
238
240
|
ammo.fire(429, {
|
|
239
241
|
error: 'Rate limit exceeded',
|
|
240
242
|
message: 'Please slow down and try again later',
|
|
241
|
-
retryAfter: ammo.res.getHeader('Retry-After')
|
|
243
|
+
retryAfter: ammo.res.getHeader('Retry-After'),
|
|
242
244
|
});
|
|
243
|
-
}
|
|
245
|
+
},
|
|
244
246
|
});
|
|
245
247
|
```
|
|
246
248
|
|
|
@@ -259,14 +261,14 @@ const api = new Target('/api');
|
|
|
259
261
|
const authLimiter = rateLimiter({
|
|
260
262
|
maxRequests: 5,
|
|
261
263
|
timeWindowSeconds: 60,
|
|
262
|
-
algorithm: 'fixed-window'
|
|
264
|
+
algorithm: 'fixed-window',
|
|
263
265
|
});
|
|
264
266
|
|
|
265
267
|
// Relaxed limit for read operations
|
|
266
268
|
const readLimiter = rateLimiter({
|
|
267
269
|
maxRequests: 1000,
|
|
268
270
|
timeWindowSeconds: 60,
|
|
269
|
-
algorithm: 'sliding-window'
|
|
271
|
+
algorithm: 'sliding-window',
|
|
270
272
|
});
|
|
271
273
|
|
|
272
274
|
// Apply to specific routes
|
|
@@ -281,11 +283,11 @@ api.register('/data', readLimiter, (ammo) => {
|
|
|
281
283
|
|
|
282
284
|
## Algorithm Comparison
|
|
283
285
|
|
|
284
|
-
| Algorithm
|
|
285
|
-
|
|
286
|
-
| **Sliding Window** | Most APIs
|
|
287
|
-
| **Token Bucket**
|
|
288
|
-
| **Fixed Window**
|
|
286
|
+
| Algorithm | Best For | Burst Handling | Accuracy | Memory |
|
|
287
|
+
| ------------------ | ------------------- | ------------------ | -------- | ------ |
|
|
288
|
+
| **Sliding Window** | Most APIs | Smooth | High | Medium |
|
|
289
|
+
| **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
|
|
290
|
+
| **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
|
|
289
291
|
|
|
290
292
|
## Examples
|
|
291
293
|
|
|
@@ -295,7 +297,7 @@ api.register('/data', readLimiter, (ammo) => {
|
|
|
295
297
|
const tierLimits = {
|
|
296
298
|
free: { maxRequests: 100, timeWindowSeconds: 3600 },
|
|
297
299
|
pro: { maxRequests: 1000, timeWindowSeconds: 3600 },
|
|
298
|
-
enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 }
|
|
300
|
+
enterprise: { maxRequests: 10000, timeWindowSeconds: 3600 },
|
|
299
301
|
};
|
|
300
302
|
|
|
301
303
|
app.withRateLimit({
|
|
@@ -309,8 +311,8 @@ app.withRateLimit({
|
|
|
309
311
|
getLimits: (key) => {
|
|
310
312
|
const tier = key.split(':')[0];
|
|
311
313
|
return tierLimits[tier] || tierLimits.free;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
+
},
|
|
315
|
+
},
|
|
314
316
|
});
|
|
315
317
|
```
|
|
316
318
|
|
|
@@ -320,14 +322,13 @@ app.withRateLimit({
|
|
|
320
322
|
// Global rate limit
|
|
321
323
|
app.withRateLimit({
|
|
322
324
|
maxRequests: 1000,
|
|
323
|
-
timeWindowSeconds: 60
|
|
325
|
+
timeWindowSeconds: 60,
|
|
324
326
|
});
|
|
325
327
|
|
|
326
328
|
// Stricter limit for expensive endpoints
|
|
327
329
|
const expensiveLimiter = rateLimiter({
|
|
328
330
|
maxRequests: 10,
|
|
329
331
|
timeWindowSeconds: 60,
|
|
330
|
-
store: 'redis'
|
|
331
332
|
});
|
|
332
333
|
|
|
333
334
|
api.register('/search', expensiveLimiter, (ammo) => {
|
|
@@ -348,7 +349,7 @@ target.register('/status', (ammo) => {
|
|
|
348
349
|
ammo.fire({
|
|
349
350
|
limit: ammo.res.getHeader('RateLimit-Limit'),
|
|
350
351
|
remaining: ammo.res.getHeader('RateLimit-Remaining'),
|
|
351
|
-
reset: ammo.res.getHeader('RateLimit-Reset')
|
|
352
|
+
reset: ammo.res.getHeader('RateLimit-Reset'),
|
|
352
353
|
});
|
|
353
354
|
});
|
|
354
355
|
```
|
|
@@ -380,11 +381,11 @@ class PostgresStorage extends RateLimitStorage {
|
|
|
380
381
|
}
|
|
381
382
|
```
|
|
382
383
|
|
|
383
|
-
The built-in
|
|
384
|
+
The built-in `MemoryStorage` and `RedisStorage` backends both extend this base class.
|
|
384
385
|
|
|
385
386
|
## Best Practices
|
|
386
387
|
|
|
387
|
-
1. **Use Redis in production** — Memory store doesn't
|
|
388
|
+
1. **Use Redis in production** — Memory store doesn't share counters across instances; use `store: { type: 'redis', url: '...' }` for distributed deployments
|
|
388
389
|
2. **Set appropriate limits** — Too strict frustrates users, too lenient invites abuse
|
|
389
390
|
3. **Different limits for different endpoints** — Auth endpoints need stricter limits
|
|
390
391
|
4. **Include headers** — Help clients self-regulate
|
package/lib/llm/client.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-4o-mini';
|
|
9
|
-
const DEFAULT_TIMEOUT =
|
|
9
|
+
const DEFAULT_TIMEOUT = 20000;
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* OpenAI-compatible LLM provider. Exposes only constructor and analyze(prompt).
|
|
@@ -20,7 +20,7 @@ class LLMProvider {
|
|
|
20
20
|
typeof options.timeout === 'number' && options.timeout > 0
|
|
21
21
|
? options.timeout
|
|
22
22
|
: DEFAULT_TIMEOUT;
|
|
23
|
-
this.options = options;
|
|
23
|
+
this.options = Object.freeze({ ...options });
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -30,6 +30,11 @@ class LLMProvider {
|
|
|
30
30
|
* @returns {Promise<{ content: string, usage: { prompt_tokens: number, completion_tokens: number, total_tokens: number } }>}
|
|
31
31
|
*/
|
|
32
32
|
async analyze(prompt) {
|
|
33
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
34
|
+
throw new TypeError(
|
|
35
|
+
'LLMProvider.analyze: prompt must be a non-empty string',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
33
38
|
const url = `${this.baseURL}/chat/completions`;
|
|
34
39
|
const headers = {
|
|
35
40
|
'Content-Type': 'application/json',
|
package/lib/llm/index.js
CHANGED
|
@@ -3,5 +3,18 @@
|
|
|
3
3
|
* Used by auto-docs, error-inference, and future LLM features.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* OpenAI-compatible LLM client.
|
|
8
|
+
* @see {@link ./client.js}
|
|
9
|
+
*/
|
|
6
10
|
export { LLMProvider, createProvider } from './client.js';
|
|
7
|
-
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* JSON parsing utilities for LLM responses.
|
|
14
|
+
* @see {@link ./parse.js}
|
|
15
|
+
*/
|
|
16
|
+
export {
|
|
17
|
+
extractJSON,
|
|
18
|
+
extractJSONArray,
|
|
19
|
+
reconcileOrderedTags,
|
|
20
|
+
} from './parse.js';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for lib/llm parse utilities (extractJSON, extractJSONArray, reconcileOrderedTags).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
extractJSON,
|
|
7
|
+
extractJSONArray,
|
|
8
|
+
reconcileOrderedTags,
|
|
9
|
+
} from './index.js';
|
|
10
|
+
|
|
11
|
+
describe('llm/parse', () => {
|
|
12
|
+
describe('extractJSON', () => {
|
|
13
|
+
it('extracts object from plain JSON string', () => {
|
|
14
|
+
const str = '{"name":"Users","description":"CRUD"}';
|
|
15
|
+
expect(extractJSON(str)).toEqual({ name: 'Users', description: 'CRUD' });
|
|
16
|
+
});
|
|
17
|
+
it('extracts first object from text with markdown', () => {
|
|
18
|
+
const str = 'Here is the result:\n```json\n{"summary":"Get item"}\n```';
|
|
19
|
+
expect(extractJSON(str)).toEqual({ summary: 'Get item' });
|
|
20
|
+
});
|
|
21
|
+
it('returns null for empty or no object', () => {
|
|
22
|
+
expect(extractJSON('')).toBeNull();
|
|
23
|
+
expect(extractJSON('no brace here')).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('extractJSONArray', () => {
|
|
28
|
+
it('extracts array from string', () => {
|
|
29
|
+
const str = '["Users", "Auth", "Health"]';
|
|
30
|
+
expect(extractJSONArray(str)).toEqual(['Users', 'Auth', 'Health']);
|
|
31
|
+
});
|
|
32
|
+
it('returns null when no array', () => {
|
|
33
|
+
expect(extractJSONArray('')).toBeNull();
|
|
34
|
+
expect(extractJSONArray('nothing')).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('reconcileOrderedTags', () => {
|
|
39
|
+
it('reorders tags by orderedTagNames', () => {
|
|
40
|
+
const tags = [
|
|
41
|
+
{ name: 'Health', description: '...' },
|
|
42
|
+
{ name: 'Users', description: '...' },
|
|
43
|
+
{ name: 'Auth', description: '...' },
|
|
44
|
+
];
|
|
45
|
+
const ordered = reconcileOrderedTags(['Users', 'Auth', 'Health'], tags);
|
|
46
|
+
expect(ordered.map((t) => t.name)).toEqual(['Users', 'Auth', 'Health']);
|
|
47
|
+
});
|
|
48
|
+
it('appends tags not in orderedTagNames', () => {
|
|
49
|
+
const tags = [{ name: 'Users' }, { name: 'Other' }];
|
|
50
|
+
const ordered = reconcileOrderedTags(['Users'], tags);
|
|
51
|
+
expect(ordered.map((t) => t.name)).toEqual(['Users', 'Other']);
|
|
52
|
+
});
|
|
53
|
+
it('returns copy of tags when orderedTagNames empty', () => {
|
|
54
|
+
const tags = [{ name: 'A' }];
|
|
55
|
+
const ordered = reconcileOrderedTags([], tags);
|
|
56
|
+
expect(ordered).toEqual([{ name: 'A' }]);
|
|
57
|
+
expect(ordered).not.toBe(tags);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "te.js",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "AI Native Node.js Framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "te.js",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.12.5",
|
|
21
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
21
22
|
"husky": "^9.0.11",
|
|
22
23
|
"lint-staged": "^15.2.2",
|
|
23
24
|
"prettier": "3.2.5",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"te.js",
|
|
32
33
|
"cli",
|
|
33
34
|
"cors",
|
|
35
|
+
"radar",
|
|
34
36
|
"server",
|
|
35
37
|
"database",
|
|
36
38
|
"rate-limit",
|