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
@@ -2,25 +2,25 @@ import RateLimitStorage from './base.js';
2
2
 
3
3
  /**
4
4
  * In-memory storage implementation for rate limiting
5
- *
5
+ *
6
6
  * @extends RateLimitStorage
7
7
  * @description
8
8
  * This storage backend uses a JavaScript Map to store rate limit data in memory.
9
- * It's suitable for single-instance applications or testing environments, but not
9
+ * It's suitable for single-instance applications or testing environments, but not
10
10
  * recommended for production use in distributed systems as data is not shared
11
11
  * between instances.
12
- *
12
+ *
13
13
  * Key features:
14
14
  * - Fast access (all data in memory)
15
15
  * - Automatic cleanup of expired entries
16
16
  * - No external dependencies
17
17
  * - Data is lost on process restart
18
18
  * - Not suitable for distributed systems
19
- *
19
+ *
20
20
  * @example
21
21
  * import { TokenBucketRateLimiter, MemoryStorage } from 'te.js/rate-limit';
22
- *
23
- * // Memory storage is used by default if no redis config is provided
22
+ *
23
+ * // Memory storage is the default storage backend
24
24
  * const limiter = new TokenBucketRateLimiter({
25
25
  * maxRequests: 60,
26
26
  * timeWindowSeconds: 60,
@@ -29,7 +29,7 @@ import RateLimitStorage from './base.js';
29
29
  * burstSize: 60
30
30
  * }
31
31
  * });
32
- *
32
+ *
33
33
  * // Or create storage instance explicitly
34
34
  * const storage = new MemoryStorage();
35
35
  * await storage.set('key', { counter: 5 }, 60); // Store for 60 seconds
@@ -45,7 +45,7 @@ class MemoryStorage extends RateLimitStorage {
45
45
 
46
46
  /**
47
47
  * Get stored data for a key, handling expiration
48
- *
48
+ *
49
49
  * @param {string} key - The storage key to retrieve data for
50
50
  * @returns {Promise<Object|null>} The stored data, or null if not found or expired
51
51
  */
@@ -61,7 +61,7 @@ class MemoryStorage extends RateLimitStorage {
61
61
 
62
62
  /**
63
63
  * Store data with expiration time
64
- *
64
+ *
65
65
  * @param {string} key - The storage key
66
66
  * @param {Object} value - The data to store
67
67
  * @param {number} ttl - Time-to-live in seconds
@@ -70,13 +70,13 @@ class MemoryStorage extends RateLimitStorage {
70
70
  async set(key, value, ttl) {
71
71
  this.store.set(key, {
72
72
  value,
73
- expireAt: Date.now() + ttl * 1000
73
+ expireAt: Date.now() + ttl * 1000,
74
74
  });
75
75
  }
76
76
 
77
77
  /**
78
78
  * Increment a numeric value in storage
79
- *
79
+ *
80
80
  * @param {string} key - The storage key to increment
81
81
  * @returns {Promise<number|null>} New value after increment, or null if key not found/expired
82
82
  */
@@ -90,7 +90,7 @@ class MemoryStorage extends RateLimitStorage {
90
90
 
91
91
  /**
92
92
  * Delete data for a key
93
- *
93
+ *
94
94
  * @param {string} key - The storage key to delete
95
95
  * @returns {Promise<void>}
96
96
  */
@@ -99,4 +99,4 @@ class MemoryStorage extends RateLimitStorage {
99
99
  }
100
100
  }
101
101
 
102
- export default MemoryStorage;
102
+ export default MemoryStorage;
@@ -0,0 +1,70 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import TejLogger from 'tej-logger';
5
+
6
+ const logger = new TejLogger('RedisAutoInstall');
7
+
8
+ /**
9
+ * Checks whether the `redis` npm package is available in the consuming
10
+ * project's package.json and node_modules.
11
+ *
12
+ * @returns {{ needsInstall: boolean, reason: string }}
13
+ */
14
+ export function checkRedisInstallation() {
15
+ const cwd = process.cwd();
16
+
17
+ const pkgPath = join(cwd, 'package.json');
18
+ if (!existsSync(pkgPath)) {
19
+ return {
20
+ needsInstall: true,
21
+ reason: 'No package.json found in project root',
22
+ };
23
+ }
24
+
25
+ let pkg;
26
+ try {
27
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
28
+ } catch {
29
+ return { needsInstall: true, reason: 'Unable to read package.json' };
30
+ }
31
+
32
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ const inPkgJson = 'redis' in deps;
34
+
35
+ const modulePath = join(cwd, 'node_modules', 'redis');
36
+ const inNodeModules = existsSync(modulePath);
37
+
38
+ if (inPkgJson && inNodeModules) {
39
+ return { needsInstall: false, reason: 'redis is already installed' };
40
+ }
41
+
42
+ if (inPkgJson && !inNodeModules) {
43
+ return {
44
+ needsInstall: true,
45
+ reason: 'redis is in package.json but not installed',
46
+ };
47
+ }
48
+
49
+ return { needsInstall: true, reason: 'redis is not in package.json' };
50
+ }
51
+
52
+ /**
53
+ * Synchronously installs the `redis` npm package into the consuming project.
54
+ * Throws if the installation fails.
55
+ */
56
+ export function installRedisSync() {
57
+ logger.info('Installing redis package …');
58
+ try {
59
+ execSync('npm install redis', {
60
+ cwd: process.cwd(),
61
+ stdio: 'pipe',
62
+ timeout: 60_000,
63
+ });
64
+ logger.info('redis package installed successfully.');
65
+ } catch (err) {
66
+ throw new Error(
67
+ `Failed to auto-install redis package. Install it manually with: npm install redis\n${err.message}`,
68
+ );
69
+ }
70
+ }
@@ -1,87 +1,129 @@
1
1
  import RateLimitStorage from './base.js';
2
+ import { checkRedisInstallation, installRedisSync } from './redis-install.js';
3
+ import TejLogger from 'tej-logger';
4
+
5
+ const logger = new TejLogger('RedisStorage');
2
6
 
3
7
  /**
4
- * Redis storage implementation for rate limiting
5
- *
6
- * @extends RateLimitStorage
7
- * @description
8
- * This storage backend uses Redis for distributed rate limiting across multiple application instances.
9
- * It's the recommended storage backend for production use in distributed systems as it provides
10
- * reliable rate limiting across all application instances.
8
+ * Redis-backed storage implementation for rate limiting.
11
9
  *
12
- * Key features:
13
- * - Distributed rate limiting (works across multiple app instances)
14
- * - Atomic operations for race condition prevention
15
- * - Automatic key expiration using Redis TTL
16
- * - Persistence options available through Redis configuration
17
- * - Clustering support for high availability
10
+ * Suitable for distributed / multi-instance deployments where rate-limit
11
+ * counters must be shared across processes.
18
12
  *
19
- * @example
20
- * import { TokenBucketRateLimiter } from 'te.js/rate-limit';
13
+ * The `redis` npm package is auto-installed into the consuming project
14
+ * on first use if it is not already present.
21
15
  *
22
- * // Use Redis storage for distributed rate limiting
23
- * const limiter = new TokenBucketRateLimiter({
24
- * maxRequests: 100,
25
- * timeWindowSeconds: 60,
26
- * store: 'redis', // Use Redis storage
27
- * tokenBucketConfig: {
28
- * refillRate: 2,
29
- * burstSize: 100
30
- * }
31
- * });
16
+ * @extends RateLimitStorage
32
17
  */
33
18
  class RedisStorage extends RateLimitStorage {
34
19
  /**
35
- * Initialize Redis storage with client
36
- *
37
- * @param {RedisClient} client - Connected Redis client instance
20
+ * @param {Object} config - Redis connection configuration
21
+ * @param {string} config.url - Redis connection URL (required)
22
+ * Remaining properties are forwarded to the node-redis `createClient` call.
38
23
  */
39
- constructor(client) {
24
+ constructor(config) {
40
25
  super();
41
- this.client = client;
26
+
27
+ if (!config?.url) {
28
+ throw new Error(
29
+ 'RedisStorage requires a url. Provide store: { type: "redis", url: "redis://..." }',
30
+ );
31
+ }
32
+
33
+ const { type: _type, url, ...redisOptions } = config;
34
+ this._url = url;
35
+ this._redisOptions = redisOptions;
36
+
37
+ this._client = null;
38
+ this._connectPromise = this._init();
39
+ }
40
+
41
+ /** Bootstrap: ensure redis is installed, create client, connect. */
42
+ async _init() {
43
+ const { needsInstall } = checkRedisInstallation();
44
+ if (needsInstall) {
45
+ installRedisSync();
46
+ }
47
+
48
+ // Dynamic import so the module is only resolved after auto-install
49
+ const { createClient } = await import('redis');
50
+
51
+ this._client = createClient({ url: this._url, ...this._redisOptions });
52
+
53
+ this._client.on('error', (err) => {
54
+ logger.error(`Redis client error: ${err.message}`);
55
+ });
56
+
57
+ await this._client.connect();
58
+ logger.info(`Connected to Redis at ${this._url}`);
59
+ }
60
+
61
+ /** Wait until the client is ready before any operation. */
62
+ async _ready() {
63
+ await this._connectPromise;
64
+ return this._client;
42
65
  }
43
66
 
44
67
  /**
45
- * Get stored data for a key
46
- *
47
- * @param {string} key - The storage key to retrieve
48
- * @returns {Promise<Object|null>} Stored value if found, null otherwise
68
+ * @param {string} key
69
+ * @returns {Promise<Object|null>}
49
70
  */
50
71
  async get(key) {
51
- const value = await this.client.get(key);
52
- return value ? JSON.parse(value) : null;
72
+ const client = await this._ready();
73
+ const raw = await client.get(key);
74
+ if (raw === null) return null;
75
+ try {
76
+ return JSON.parse(raw);
77
+ } catch {
78
+ return null;
79
+ }
53
80
  }
54
81
 
55
82
  /**
56
- * Store data with expiration time
57
- *
58
- * @param {string} key - The storage key
59
- * @param {Object} value - The data to store
60
- * @param {number} ttl - Time-to-live in seconds
61
- * @returns {Promise<void>}
83
+ * @param {string} key
84
+ * @param {Object} value
85
+ * @param {number} ttl - seconds
62
86
  */
63
87
  async set(key, value, ttl) {
64
- await this.client.set(key, JSON.stringify(value), { EX: ttl });
88
+ const client = await this._ready();
89
+ const seconds = Math.max(1, Math.ceil(ttl));
90
+ await client.setEx(key, seconds, JSON.stringify(value));
65
91
  }
66
92
 
67
93
  /**
68
- * Increment a counter value atomically
94
+ * Atomically increments the `counter` field inside the stored JSON value.
95
+ * Uses a Lua script to read-modify-write in a single round-trip.
69
96
  *
70
- * @param {string} key - The storage key to increment
71
- * @returns {Promise<number>} New value after increment
97
+ * @param {string} key
98
+ * @returns {Promise<number|null>}
72
99
  */
73
100
  async increment(key) {
74
- return await this.client.incr(key);
101
+ const client = await this._ready();
102
+
103
+ const script = `
104
+ local raw = redis.call('GET', KEYS[1])
105
+ if not raw then return nil end
106
+ local data = cjson.decode(raw)
107
+ data.counter = (data.counter or 0) + 1
108
+ local ttl = redis.call('TTL', KEYS[1])
109
+ if ttl > 0 then
110
+ redis.call('SETEX', KEYS[1], ttl, cjson.encode(data))
111
+ else
112
+ redis.call('SET', KEYS[1], cjson.encode(data))
113
+ end
114
+ return data.counter
115
+ `;
116
+
117
+ const result = await client.eval(script, { keys: [key] });
118
+ return result === null ? null : Number(result);
75
119
  }
76
120
 
77
121
  /**
78
- * Delete data for a key
79
- *
80
- * @param {string} key - The storage key to delete
81
- * @returns {Promise<void>}
122
+ * @param {string} key
82
123
  */
83
124
  async delete(key) {
84
- await this.client.del(key);
125
+ const client = await this._ready();
126
+ await client.del(key);
85
127
  }
86
128
  }
87
129