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
|
@@ -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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
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(
|
|
24
|
+
constructor(config) {
|
|
40
25
|
super();
|
|
41
|
-
|
|
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
|
-
*
|
|
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
|
|
52
|
-
|
|
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
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* @param {
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
71
|
-
* @returns {Promise<number>}
|
|
97
|
+
* @param {string} key
|
|
98
|
+
* @returns {Promise<number|null>}
|
|
72
99
|
*/
|
|
73
100
|
async increment(key) {
|
|
74
|
-
|
|
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
|
-
*
|
|
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.
|
|
125
|
+
const client = await this._ready();
|
|
126
|
+
await client.del(key);
|
|
85
127
|
}
|
|
86
128
|
}
|
|
87
129
|
|