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
@@ -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, // Maximum requests in time window
29
- timeWindowSeconds: 60, // Time window in seconds
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: 'redis', // 'redis' | 'memory'
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', // 'standard' | 'legacy' | 'both'
49
- draft7: false, // Include RateLimit-Policy header (e.g. "100;w=60")
50
- draft8: false // Use delta-seconds for RateLimit-Reset instead of Unix timestamp
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, // Tokens per second (100/60)
87
- burstSize: 150 // Maximum tokens (allows 50% burst)
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 // Align to clock boundaries
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
- Required for distributed/multi-server deployments:
131
+ For distributed / multi-instance deployments where counters must be shared:
131
132
 
132
133
  ```javascript
133
- app
134
- .withRedis({ url: 'redis://localhost:6379' })
135
- .withRateLimit({
136
- maxRequests: 100,
137
- timeWindowSeconds: 60,
138
- store: 'redis'
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
- **Pros:** Shared across all servers, persistent
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
- > **Important:** Initialize Redis with `withRedis()` before using `store: 'redis'`
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 | Best For | Burst Handling | Accuracy | Memory |
285
- |-----------|----------|----------------|----------|--------|
286
- | **Sliding Window** | Most APIs | Smooth | High | Medium |
287
- | **Token Bucket** | Burst-tolerant APIs | Allows bursts | Medium | Low |
288
- | **Fixed Window** | Simple cases | Poor at boundaries | Low | Low |
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 backends (`MemoryStorage` and `RedisStorage`) both extend this base class.
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 scale across instances
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 = 10000;
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
- export { extractJSON, extractJSONArray, reconcileOrderedTags } from './parse.js';
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.6",
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",