te.js 2.1.0 → 2.1.2
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 +197 -196
- package/auto-docs/analysis/handler-analyzer.js +58 -58
- package/auto-docs/analysis/source-resolver.js +101 -101
- package/auto-docs/constants.js +37 -37
- package/auto-docs/docs-llm/index.js +7 -7
- package/auto-docs/docs-llm/prompts.js +222 -222
- package/auto-docs/docs-llm/provider.js +132 -132
- package/auto-docs/index.js +146 -146
- package/auto-docs/openapi/endpoint-processor.js +277 -277
- package/auto-docs/openapi/generator.js +107 -107
- package/auto-docs/openapi/level3.js +131 -131
- package/auto-docs/openapi/spec-builders.js +244 -244
- package/auto-docs/ui/docs-ui.js +186 -186
- package/auto-docs/utils/logger.js +17 -17
- package/auto-docs/utils/strip-usage.js +10 -10
- package/cli/docs-command.js +315 -315
- package/cli/fly-command.js +71 -71
- package/cli/index.js +56 -56
- package/cors/index.js +71 -0
- package/database/index.js +165 -165
- package/database/mongodb.js +146 -146
- package/database/redis.js +201 -201
- package/docs/README.md +36 -36
- package/docs/ammo.md +362 -362
- package/docs/api-reference.md +490 -490
- package/docs/auto-docs.md +216 -216
- package/docs/cli.md +152 -152
- package/docs/configuration.md +275 -275
- package/docs/database.md +390 -390
- package/docs/error-handling.md +438 -438
- package/docs/file-uploads.md +333 -333
- package/docs/getting-started.md +214 -214
- package/docs/middleware.md +355 -355
- package/docs/rate-limiting.md +393 -393
- package/docs/routing.md +302 -302
- package/lib/llm/client.js +73 -0
- package/lib/llm/index.js +7 -0
- package/lib/llm/parse.js +89 -0
- package/package.json +64 -62
- package/rate-limit/algorithms/fixed-window.js +141 -141
- package/rate-limit/algorithms/sliding-window.js +147 -147
- package/rate-limit/algorithms/token-bucket.js +115 -115
- package/rate-limit/base.js +165 -165
- package/rate-limit/index.js +147 -147
- package/rate-limit/storage/base.js +104 -104
- package/rate-limit/storage/memory.js +101 -101
- package/rate-limit/storage/redis.js +88 -88
- package/server/ammo/body-parser.js +220 -220
- package/server/ammo/dispatch-helper.js +103 -103
- package/server/ammo/enhancer.js +57 -57
- package/server/ammo.js +454 -415
- package/server/endpoint.js +97 -74
- package/server/error.js +9 -9
- package/server/errors/code-context.js +125 -125
- package/server/errors/llm-error-service.js +140 -140
- package/server/files/helper.js +33 -33
- package/server/files/uploader.js +143 -143
- package/server/handler.js +158 -119
- package/server/target.js +185 -175
- package/server/targets/middleware-validator.js +22 -22
- package/server/targets/path-validator.js +21 -21
- package/server/targets/registry.js +160 -160
- package/server/targets/shoot-validator.js +21 -21
- package/te.js +428 -402
- package/utils/auto-register.js +17 -17
- package/utils/configuration.js +64 -64
- package/utils/errors-llm-config.js +84 -84
- package/utils/request-logger.js +43 -43
- package/utils/status-codes.js +82 -82
- package/utils/tejas-entrypoint-html.js +18 -18
package/rate-limit/base.js
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
import TejError from '../server/error.js';
|
|
2
|
-
import MemoryStorage from './storage/memory.js';
|
|
3
|
-
import RedisStorage from './storage/redis.js';
|
|
4
|
-
import dbManager from '../database/index.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Base rate limiter class implementing common functionality for rate limiting algorithms
|
|
8
|
-
*
|
|
9
|
-
* @abstract
|
|
10
|
-
* @class
|
|
11
|
-
* @description
|
|
12
|
-
* This is the base class for all rate limiting algorithms. It provides common configuration
|
|
13
|
-
* options and storage handling, while allowing specific algorithms to implement their own logic.
|
|
14
|
-
* Only one algorithm can be active per instance - the algorithm is determined by which options
|
|
15
|
-
* object is provided (tokenBucketConfig, slidingWindowConfig, or fixedWindowConfig).
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* // Using with Redis storage and token bucket algorithm
|
|
19
|
-
* const limiter = new TokenBucketRateLimiter({
|
|
20
|
-
* maxRequests: 10,
|
|
21
|
-
* timeWindowSeconds: 60,
|
|
22
|
-
* store: 'redis',
|
|
23
|
-
* tokenBucketConfig: {
|
|
24
|
-
* refillRate: 0.5,
|
|
25
|
-
* burstSize: 15
|
|
26
|
-
* }
|
|
27
|
-
* });
|
|
28
|
-
*/
|
|
29
|
-
class RateLimiter {
|
|
30
|
-
/**
|
|
31
|
-
* Creates a new rate limiter instance
|
|
32
|
-
*
|
|
33
|
-
* @param {Object} options - Configuration options for the rate limiter
|
|
34
|
-
* @param {number} [options.maxRequests=60] - Maximum number of requests allowed within the time window.
|
|
35
|
-
* This is the default rate limit cap that applies across all algorithms.
|
|
36
|
-
* For token bucket, this affects the default refill rate.
|
|
37
|
-
* @param {number} [options.timeWindowSeconds=60] - Time window in seconds for rate limiting.
|
|
38
|
-
* For fixed window, this is the window duration.
|
|
39
|
-
* For sliding window, this is the total time span considered.
|
|
40
|
-
* For token bucket, this affects the default refill rate calculation.
|
|
41
|
-
* @param {string} [options.keyPrefix='rl:'] - Prefix for storage keys. Useful when implementing different rate limit
|
|
42
|
-
* rules with different prefixes (e.g., 'rl:api:', 'rl:web:').
|
|
43
|
-
* @param {string} [options.store='memory'] - Storage backend to use ('memory' or 'redis')
|
|
44
|
-
* @param {Object} [options.tokenBucketConfig] - Token bucket algorithm specific options
|
|
45
|
-
* @param {Object} [options.slidingWindowConfig] - Sliding window algorithm specific options
|
|
46
|
-
* @param {Object} [options.fixedWindowConfig] - Fixed window algorithm specific options
|
|
47
|
-
*/
|
|
48
|
-
constructor(options) {
|
|
49
|
-
// Common options for all algorithms
|
|
50
|
-
this.options = {
|
|
51
|
-
maxRequests: 60, // Maximum number of requests
|
|
52
|
-
timeWindowSeconds: 60, // Time window in seconds
|
|
53
|
-
keyPrefix: 'rl:', // Key prefix for storage
|
|
54
|
-
store: 'memory', // Default to memory storage
|
|
55
|
-
...options,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Only one algorithm can be active per instance
|
|
59
|
-
if (options?.tokenBucketConfig && options?.slidingWindowConfig) {
|
|
60
|
-
throw new TejError(
|
|
61
|
-
400,
|
|
62
|
-
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (options?.tokenBucketConfig && options?.fixedWindowConfig) {
|
|
67
|
-
throw new TejError(
|
|
68
|
-
500,
|
|
69
|
-
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (options?.slidingWindowConfig && options?.fixedWindowConfig) {
|
|
74
|
-
throw new TejError(
|
|
75
|
-
500,
|
|
76
|
-
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Set default values for algorithm options if any are provided
|
|
81
|
-
this.tokenBucketOptions = options?.tokenBucketConfig
|
|
82
|
-
? {
|
|
83
|
-
refillRate: this.options.maxRequests / this.options.timeWindowSeconds, // Tokens per second
|
|
84
|
-
burstSize: this.options.maxRequests, // Maximum token capacity
|
|
85
|
-
...options.tokenBucketConfig,
|
|
86
|
-
}
|
|
87
|
-
: null;
|
|
88
|
-
|
|
89
|
-
this.slidingWindowOptions = options?.slidingWindowConfig
|
|
90
|
-
? {
|
|
91
|
-
granularity: 1, // Time precision in seconds
|
|
92
|
-
weights: { current: 1, previous: 0 }, // Weights for current and previous windows
|
|
93
|
-
...options.slidingWindowConfig,
|
|
94
|
-
}
|
|
95
|
-
: null;
|
|
96
|
-
|
|
97
|
-
this.fixedWindowOptions = options?.fixedWindowConfig
|
|
98
|
-
? {
|
|
99
|
-
strictWindow: false, // If true, windows align with clock
|
|
100
|
-
...options.fixedWindowConfig,
|
|
101
|
-
}
|
|
102
|
-
: null;
|
|
103
|
-
|
|
104
|
-
// Initialize storage based on store type
|
|
105
|
-
if (this.options.store === 'redis') {
|
|
106
|
-
if (!dbManager.hasConnection('redis')) {
|
|
107
|
-
throw new TejError(
|
|
108
|
-
500,
|
|
109
|
-
'Redis store selected but no Redis connection available. Call withRedis() first.',
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
const redisClient = dbManager.getConnection('redis');
|
|
113
|
-
this.storage = new RedisStorage(redisClient);
|
|
114
|
-
} else {
|
|
115
|
-
this.storage = new MemoryStorage();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Generate storage key for the rate limit identifier
|
|
121
|
-
*
|
|
122
|
-
* @param {string} identifier - Unique identifier for the rate limit (e.g. IP address, user ID)
|
|
123
|
-
* @returns {string} The storage key with prefix
|
|
124
|
-
*/
|
|
125
|
-
getKey(identifier) {
|
|
126
|
-
return `${this.options.keyPrefix}${identifier}`;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Abstract method for checking if request is allowed
|
|
131
|
-
* Must be implemented by concrete rate limiter classes
|
|
132
|
-
*
|
|
133
|
-
* @abstract
|
|
134
|
-
* @param {string} identifier - Unique identifier for the rate limit (e.g. IP address, user ID)
|
|
135
|
-
* @returns {Promise<Object>} Rate limit check result
|
|
136
|
-
* @returns {boolean} result.success - Whether the request is allowed
|
|
137
|
-
* @returns {number} result.remainingRequests - Number of requests remaining in the window
|
|
138
|
-
* @returns {number} result.resetTime - Unix timestamp when the rate limit resets
|
|
139
|
-
* @throws {Error} If not implemented by child class
|
|
140
|
-
*/
|
|
141
|
-
async consume(identifier) {
|
|
142
|
-
throw new TejError(500, 'Not implemented');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get algorithm-specific options for the specified algorithm type
|
|
147
|
-
*
|
|
148
|
-
* @param {string} type - Algorithm type ('tokenBucketConfig', 'slidingWindowConfig', or 'fixedWindowConfig')
|
|
149
|
-
* @returns {Object|null} The algorithm-specific options, or null if type not found
|
|
150
|
-
*/
|
|
151
|
-
getAlgorithmOptions(type) {
|
|
152
|
-
switch (type) {
|
|
153
|
-
case 'token-bucket':
|
|
154
|
-
return this.tokenBucketOptions;
|
|
155
|
-
case 'sliding-window':
|
|
156
|
-
return this.slidingWindowOptions;
|
|
157
|
-
case 'fixed-window':
|
|
158
|
-
return this.fixedWindowOptions;
|
|
159
|
-
default:
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export default RateLimiter;
|
|
1
|
+
import TejError from '../server/error.js';
|
|
2
|
+
import MemoryStorage from './storage/memory.js';
|
|
3
|
+
import RedisStorage from './storage/redis.js';
|
|
4
|
+
import dbManager from '../database/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base rate limiter class implementing common functionality for rate limiting algorithms
|
|
8
|
+
*
|
|
9
|
+
* @abstract
|
|
10
|
+
* @class
|
|
11
|
+
* @description
|
|
12
|
+
* This is the base class for all rate limiting algorithms. It provides common configuration
|
|
13
|
+
* options and storage handling, while allowing specific algorithms to implement their own logic.
|
|
14
|
+
* Only one algorithm can be active per instance - the algorithm is determined by which options
|
|
15
|
+
* object is provided (tokenBucketConfig, slidingWindowConfig, or fixedWindowConfig).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Using with Redis storage and token bucket algorithm
|
|
19
|
+
* const limiter = new TokenBucketRateLimiter({
|
|
20
|
+
* maxRequests: 10,
|
|
21
|
+
* timeWindowSeconds: 60,
|
|
22
|
+
* store: 'redis',
|
|
23
|
+
* tokenBucketConfig: {
|
|
24
|
+
* refillRate: 0.5,
|
|
25
|
+
* burstSize: 15
|
|
26
|
+
* }
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
class RateLimiter {
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new rate limiter instance
|
|
32
|
+
*
|
|
33
|
+
* @param {Object} options - Configuration options for the rate limiter
|
|
34
|
+
* @param {number} [options.maxRequests=60] - Maximum number of requests allowed within the time window.
|
|
35
|
+
* This is the default rate limit cap that applies across all algorithms.
|
|
36
|
+
* For token bucket, this affects the default refill rate.
|
|
37
|
+
* @param {number} [options.timeWindowSeconds=60] - Time window in seconds for rate limiting.
|
|
38
|
+
* For fixed window, this is the window duration.
|
|
39
|
+
* For sliding window, this is the total time span considered.
|
|
40
|
+
* For token bucket, this affects the default refill rate calculation.
|
|
41
|
+
* @param {string} [options.keyPrefix='rl:'] - Prefix for storage keys. Useful when implementing different rate limit
|
|
42
|
+
* rules with different prefixes (e.g., 'rl:api:', 'rl:web:').
|
|
43
|
+
* @param {string} [options.store='memory'] - Storage backend to use ('memory' or 'redis')
|
|
44
|
+
* @param {Object} [options.tokenBucketConfig] - Token bucket algorithm specific options
|
|
45
|
+
* @param {Object} [options.slidingWindowConfig] - Sliding window algorithm specific options
|
|
46
|
+
* @param {Object} [options.fixedWindowConfig] - Fixed window algorithm specific options
|
|
47
|
+
*/
|
|
48
|
+
constructor(options) {
|
|
49
|
+
// Common options for all algorithms
|
|
50
|
+
this.options = {
|
|
51
|
+
maxRequests: 60, // Maximum number of requests
|
|
52
|
+
timeWindowSeconds: 60, // Time window in seconds
|
|
53
|
+
keyPrefix: 'rl:', // Key prefix for storage
|
|
54
|
+
store: 'memory', // Default to memory storage
|
|
55
|
+
...options,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Only one algorithm can be active per instance
|
|
59
|
+
if (options?.tokenBucketConfig && options?.slidingWindowConfig) {
|
|
60
|
+
throw new TejError(
|
|
61
|
+
400,
|
|
62
|
+
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (options?.tokenBucketConfig && options?.fixedWindowConfig) {
|
|
67
|
+
throw new TejError(
|
|
68
|
+
500,
|
|
69
|
+
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options?.slidingWindowConfig && options?.fixedWindowConfig) {
|
|
74
|
+
throw new TejError(
|
|
75
|
+
500,
|
|
76
|
+
'Cannot use multiple rate limiting algorithms. Choose either tokenBucketConfig or slidingWindowConfig or fixedWindowConfig.',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Set default values for algorithm options if any are provided
|
|
81
|
+
this.tokenBucketOptions = options?.tokenBucketConfig
|
|
82
|
+
? {
|
|
83
|
+
refillRate: this.options.maxRequests / this.options.timeWindowSeconds, // Tokens per second
|
|
84
|
+
burstSize: this.options.maxRequests, // Maximum token capacity
|
|
85
|
+
...options.tokenBucketConfig,
|
|
86
|
+
}
|
|
87
|
+
: null;
|
|
88
|
+
|
|
89
|
+
this.slidingWindowOptions = options?.slidingWindowConfig
|
|
90
|
+
? {
|
|
91
|
+
granularity: 1, // Time precision in seconds
|
|
92
|
+
weights: { current: 1, previous: 0 }, // Weights for current and previous windows
|
|
93
|
+
...options.slidingWindowConfig,
|
|
94
|
+
}
|
|
95
|
+
: null;
|
|
96
|
+
|
|
97
|
+
this.fixedWindowOptions = options?.fixedWindowConfig
|
|
98
|
+
? {
|
|
99
|
+
strictWindow: false, // If true, windows align with clock
|
|
100
|
+
...options.fixedWindowConfig,
|
|
101
|
+
}
|
|
102
|
+
: null;
|
|
103
|
+
|
|
104
|
+
// Initialize storage based on store type
|
|
105
|
+
if (this.options.store === 'redis') {
|
|
106
|
+
if (!dbManager.hasConnection('redis')) {
|
|
107
|
+
throw new TejError(
|
|
108
|
+
500,
|
|
109
|
+
'Redis store selected but no Redis connection available. Call withRedis() first.',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const redisClient = dbManager.getConnection('redis');
|
|
113
|
+
this.storage = new RedisStorage(redisClient);
|
|
114
|
+
} else {
|
|
115
|
+
this.storage = new MemoryStorage();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generate storage key for the rate limit identifier
|
|
121
|
+
*
|
|
122
|
+
* @param {string} identifier - Unique identifier for the rate limit (e.g. IP address, user ID)
|
|
123
|
+
* @returns {string} The storage key with prefix
|
|
124
|
+
*/
|
|
125
|
+
getKey(identifier) {
|
|
126
|
+
return `${this.options.keyPrefix}${identifier}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Abstract method for checking if request is allowed
|
|
131
|
+
* Must be implemented by concrete rate limiter classes
|
|
132
|
+
*
|
|
133
|
+
* @abstract
|
|
134
|
+
* @param {string} identifier - Unique identifier for the rate limit (e.g. IP address, user ID)
|
|
135
|
+
* @returns {Promise<Object>} Rate limit check result
|
|
136
|
+
* @returns {boolean} result.success - Whether the request is allowed
|
|
137
|
+
* @returns {number} result.remainingRequests - Number of requests remaining in the window
|
|
138
|
+
* @returns {number} result.resetTime - Unix timestamp when the rate limit resets
|
|
139
|
+
* @throws {Error} If not implemented by child class
|
|
140
|
+
*/
|
|
141
|
+
async consume(identifier) {
|
|
142
|
+
throw new TejError(500, 'Not implemented');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get algorithm-specific options for the specified algorithm type
|
|
147
|
+
*
|
|
148
|
+
* @param {string} type - Algorithm type ('tokenBucketConfig', 'slidingWindowConfig', or 'fixedWindowConfig')
|
|
149
|
+
* @returns {Object|null} The algorithm-specific options, or null if type not found
|
|
150
|
+
*/
|
|
151
|
+
getAlgorithmOptions(type) {
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'token-bucket':
|
|
154
|
+
return this.tokenBucketOptions;
|
|
155
|
+
case 'sliding-window':
|
|
156
|
+
return this.slidingWindowOptions;
|
|
157
|
+
case 'fixed-window':
|
|
158
|
+
return this.fixedWindowOptions;
|
|
159
|
+
default:
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export default RateLimiter;
|
package/rate-limit/index.js
CHANGED
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import TejError from '../server/error.js';
|
|
2
|
-
import FixedWindowRateLimiter from './algorithms/fixed-window.js';
|
|
3
|
-
import SlidingWindowRateLimiter from './algorithms/sliding-window.js';
|
|
4
|
-
import TokenBucketRateLimiter from './algorithms/token-bucket.js';
|
|
5
|
-
import dbManager from '../database/index.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a rate limiting middleware function with the specified algorithm and storage
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} options - Configuration options for the rate limiter
|
|
11
|
-
* @param {number} options.maxRequests - Maximum number of requests allowed within the time window
|
|
12
|
-
* @param {number} options.timeWindowSeconds - Time window in seconds
|
|
13
|
-
* @param {string} [options.algorithm='sliding-window'] - Rate limiting algorithm to use:
|
|
14
|
-
* - 'token-bucket': Best for handling traffic bursts
|
|
15
|
-
* - 'sliding-window': Best for smooth rate limiting
|
|
16
|
-
* - 'fixed-window': Simplest approach
|
|
17
|
-
* @param {string} [options.store='memory'] - Storage backend to use:
|
|
18
|
-
* - 'memory': In-memory storage (default)
|
|
19
|
-
* - 'redis': Redis-based storage (requires global Redis config)
|
|
20
|
-
* @param {Object} [options.algorithmOptions] - Algorithm-specific options
|
|
21
|
-
* @param {Function} [options.keyGenerator] - Optional function to generate unique identifiers
|
|
22
|
-
* @param {Object} [options.headerFormat] - Rate limit header format configuration
|
|
23
|
-
* @param {string} [options.headerFormat.type='standard'] - Type of headers to use:
|
|
24
|
-
* - 'legacy': Use X-RateLimit-* headers
|
|
25
|
-
* - 'standard': Use RateLimit-* headers (draft 6+)
|
|
26
|
-
* - 'both': Use both legacy and standard headers
|
|
27
|
-
* @param {boolean} [options.headerFormat.draft7=false] - Whether to include draft 7 policy header
|
|
28
|
-
* @param {boolean} [options.headerFormat.draft8=false] - Whether to include draft 8 reset format
|
|
29
|
-
* @param {Function} [options.onRateLimited] - Optional callback when rate limit is exceeded
|
|
30
|
-
* @returns {Function} Middleware function for use with te.js
|
|
31
|
-
*/
|
|
32
|
-
function rateLimiter(options) {
|
|
33
|
-
const {
|
|
34
|
-
algorithm = 'sliding-window',
|
|
35
|
-
store = 'memory',
|
|
36
|
-
keyGenerator = (ammo) => ammo.ip,
|
|
37
|
-
headerFormat = { type: 'standard' },
|
|
38
|
-
onRateLimited,
|
|
39
|
-
...limiterOptions
|
|
40
|
-
} = options;
|
|
41
|
-
|
|
42
|
-
// Check Redis connectivity if Redis store is selected
|
|
43
|
-
if (store === 'redis' && !dbManager.hasConnection('redis', {})) {
|
|
44
|
-
throw new TejError(
|
|
45
|
-
400,
|
|
46
|
-
'Redis store selected but no Redis connection found. Please use withRedis() before using withRateLimit()',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Map algorithm names to their config property names
|
|
51
|
-
const configMap = {
|
|
52
|
-
'token-bucket': 'tokenBucketConfig',
|
|
53
|
-
'sliding-window': 'slidingWindowConfig',
|
|
54
|
-
'fixed-window': 'fixedWindowConfig',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const configKey = configMap[algorithm];
|
|
58
|
-
if (!configKey) {
|
|
59
|
-
throw new TejError(
|
|
60
|
-
400,
|
|
61
|
-
`Invalid algorithm: ${algorithm}. Must be one of: ${Object.keys(configMap).join(', ')}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Create algorithm-specific config
|
|
66
|
-
const limiterConfig = {
|
|
67
|
-
maxRequests: limiterOptions.maxRequests,
|
|
68
|
-
timeWindowSeconds: limiterOptions.timeWindowSeconds,
|
|
69
|
-
[configKey]: limiterOptions.algorithmOptions || {},
|
|
70
|
-
store, // Pass the store type to the limiter
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Create the appropriate limiter instance
|
|
74
|
-
let limiter;
|
|
75
|
-
switch (algorithm) {
|
|
76
|
-
case 'token-bucket':
|
|
77
|
-
limiter = new TokenBucketRateLimiter(limiterConfig);
|
|
78
|
-
break;
|
|
79
|
-
case 'sliding-window':
|
|
80
|
-
limiter = new SlidingWindowRateLimiter(limiterConfig);
|
|
81
|
-
break;
|
|
82
|
-
case 'fixed-window':
|
|
83
|
-
limiter = new FixedWindowRateLimiter(limiterConfig);
|
|
84
|
-
break;
|
|
85
|
-
default:
|
|
86
|
-
throw new TejError(400, 'Invalid algorithm specified');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Helper to set headers based on format
|
|
90
|
-
const setRateLimitHeaders = (ammo, result) => {
|
|
91
|
-
const { type = 'standard', draft7 = false, draft8 = false } = headerFormat;
|
|
92
|
-
const useStandard = type === 'standard' || type === 'both';
|
|
93
|
-
const useLegacy = type === 'legacy' || type === 'both';
|
|
94
|
-
|
|
95
|
-
if (useStandard) {
|
|
96
|
-
// Standard headers (draft 6+)
|
|
97
|
-
ammo.res.setHeader('RateLimit-Limit', limiter.options.maxRequests);
|
|
98
|
-
ammo.res.setHeader('RateLimit-Remaining', result.remainingRequests);
|
|
99
|
-
|
|
100
|
-
// Draft 8 uses delta-seconds format
|
|
101
|
-
if (draft8) {
|
|
102
|
-
const resetDelta = result.resetTime - Math.floor(Date.now() / 1000);
|
|
103
|
-
ammo.res.setHeader('RateLimit-Reset', resetDelta);
|
|
104
|
-
} else {
|
|
105
|
-
ammo.res.setHeader('RateLimit-Reset', result.resetTime);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Draft 7 added optional policy information
|
|
109
|
-
if (draft7) {
|
|
110
|
-
const policy = `${limiter.options.maxRequests};w=${limiter.options.timeWindowSeconds}`;
|
|
111
|
-
ammo.res.setHeader('RateLimit-Policy', policy);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (useLegacy) {
|
|
116
|
-
// Legacy X- headers
|
|
117
|
-
ammo.res.setHeader('X-RateLimit-Limit', limiter.options.maxRequests);
|
|
118
|
-
ammo.res.setHeader('X-RateLimit-Remaining', result.remainingRequests);
|
|
119
|
-
ammo.res.setHeader('X-RateLimit-Reset', result.resetTime);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Always set Retry-After on 429 responses
|
|
123
|
-
if (!result.success) {
|
|
124
|
-
const retryAfter = result.resetTime - Math.floor(Date.now() / 1000);
|
|
125
|
-
ammo.res.setHeader('Retry-After', retryAfter);
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Return middleware function
|
|
130
|
-
return async (ammo, next) => {
|
|
131
|
-
const key = keyGenerator(ammo);
|
|
132
|
-
const result = await limiter.consume(key);
|
|
133
|
-
|
|
134
|
-
setRateLimitHeaders(ammo, result);
|
|
135
|
-
|
|
136
|
-
if (!result.success) {
|
|
137
|
-
if (onRateLimited) {
|
|
138
|
-
return onRateLimited(ammo);
|
|
139
|
-
}
|
|
140
|
-
return ammo.throw(429, 'Too Many Requests');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
await next();
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export default rateLimiter;
|
|
1
|
+
import TejError from '../server/error.js';
|
|
2
|
+
import FixedWindowRateLimiter from './algorithms/fixed-window.js';
|
|
3
|
+
import SlidingWindowRateLimiter from './algorithms/sliding-window.js';
|
|
4
|
+
import TokenBucketRateLimiter from './algorithms/token-bucket.js';
|
|
5
|
+
import dbManager from '../database/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a rate limiting middleware function with the specified algorithm and storage
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options - Configuration options for the rate limiter
|
|
11
|
+
* @param {number} options.maxRequests - Maximum number of requests allowed within the time window
|
|
12
|
+
* @param {number} options.timeWindowSeconds - Time window in seconds
|
|
13
|
+
* @param {string} [options.algorithm='sliding-window'] - Rate limiting algorithm to use:
|
|
14
|
+
* - 'token-bucket': Best for handling traffic bursts
|
|
15
|
+
* - 'sliding-window': Best for smooth rate limiting
|
|
16
|
+
* - 'fixed-window': Simplest approach
|
|
17
|
+
* @param {string} [options.store='memory'] - Storage backend to use:
|
|
18
|
+
* - 'memory': In-memory storage (default)
|
|
19
|
+
* - 'redis': Redis-based storage (requires global Redis config)
|
|
20
|
+
* @param {Object} [options.algorithmOptions] - Algorithm-specific options
|
|
21
|
+
* @param {Function} [options.keyGenerator] - Optional function to generate unique identifiers
|
|
22
|
+
* @param {Object} [options.headerFormat] - Rate limit header format configuration
|
|
23
|
+
* @param {string} [options.headerFormat.type='standard'] - Type of headers to use:
|
|
24
|
+
* - 'legacy': Use X-RateLimit-* headers
|
|
25
|
+
* - 'standard': Use RateLimit-* headers (draft 6+)
|
|
26
|
+
* - 'both': Use both legacy and standard headers
|
|
27
|
+
* @param {boolean} [options.headerFormat.draft7=false] - Whether to include draft 7 policy header
|
|
28
|
+
* @param {boolean} [options.headerFormat.draft8=false] - Whether to include draft 8 reset format
|
|
29
|
+
* @param {Function} [options.onRateLimited] - Optional callback when rate limit is exceeded
|
|
30
|
+
* @returns {Function} Middleware function for use with te.js
|
|
31
|
+
*/
|
|
32
|
+
function rateLimiter(options) {
|
|
33
|
+
const {
|
|
34
|
+
algorithm = 'sliding-window',
|
|
35
|
+
store = 'memory',
|
|
36
|
+
keyGenerator = (ammo) => ammo.ip,
|
|
37
|
+
headerFormat = { type: 'standard' },
|
|
38
|
+
onRateLimited,
|
|
39
|
+
...limiterOptions
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
// Check Redis connectivity if Redis store is selected
|
|
43
|
+
if (store === 'redis' && !dbManager.hasConnection('redis', {})) {
|
|
44
|
+
throw new TejError(
|
|
45
|
+
400,
|
|
46
|
+
'Redis store selected but no Redis connection found. Please use withRedis() before using withRateLimit()',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Map algorithm names to their config property names
|
|
51
|
+
const configMap = {
|
|
52
|
+
'token-bucket': 'tokenBucketConfig',
|
|
53
|
+
'sliding-window': 'slidingWindowConfig',
|
|
54
|
+
'fixed-window': 'fixedWindowConfig',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const configKey = configMap[algorithm];
|
|
58
|
+
if (!configKey) {
|
|
59
|
+
throw new TejError(
|
|
60
|
+
400,
|
|
61
|
+
`Invalid algorithm: ${algorithm}. Must be one of: ${Object.keys(configMap).join(', ')}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create algorithm-specific config
|
|
66
|
+
const limiterConfig = {
|
|
67
|
+
maxRequests: limiterOptions.maxRequests,
|
|
68
|
+
timeWindowSeconds: limiterOptions.timeWindowSeconds,
|
|
69
|
+
[configKey]: limiterOptions.algorithmOptions || {},
|
|
70
|
+
store, // Pass the store type to the limiter
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Create the appropriate limiter instance
|
|
74
|
+
let limiter;
|
|
75
|
+
switch (algorithm) {
|
|
76
|
+
case 'token-bucket':
|
|
77
|
+
limiter = new TokenBucketRateLimiter(limiterConfig);
|
|
78
|
+
break;
|
|
79
|
+
case 'sliding-window':
|
|
80
|
+
limiter = new SlidingWindowRateLimiter(limiterConfig);
|
|
81
|
+
break;
|
|
82
|
+
case 'fixed-window':
|
|
83
|
+
limiter = new FixedWindowRateLimiter(limiterConfig);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
throw new TejError(400, 'Invalid algorithm specified');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Helper to set headers based on format
|
|
90
|
+
const setRateLimitHeaders = (ammo, result) => {
|
|
91
|
+
const { type = 'standard', draft7 = false, draft8 = false } = headerFormat;
|
|
92
|
+
const useStandard = type === 'standard' || type === 'both';
|
|
93
|
+
const useLegacy = type === 'legacy' || type === 'both';
|
|
94
|
+
|
|
95
|
+
if (useStandard) {
|
|
96
|
+
// Standard headers (draft 6+)
|
|
97
|
+
ammo.res.setHeader('RateLimit-Limit', limiter.options.maxRequests);
|
|
98
|
+
ammo.res.setHeader('RateLimit-Remaining', result.remainingRequests);
|
|
99
|
+
|
|
100
|
+
// Draft 8 uses delta-seconds format
|
|
101
|
+
if (draft8) {
|
|
102
|
+
const resetDelta = result.resetTime - Math.floor(Date.now() / 1000);
|
|
103
|
+
ammo.res.setHeader('RateLimit-Reset', resetDelta);
|
|
104
|
+
} else {
|
|
105
|
+
ammo.res.setHeader('RateLimit-Reset', result.resetTime);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Draft 7 added optional policy information
|
|
109
|
+
if (draft7) {
|
|
110
|
+
const policy = `${limiter.options.maxRequests};w=${limiter.options.timeWindowSeconds}`;
|
|
111
|
+
ammo.res.setHeader('RateLimit-Policy', policy);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (useLegacy) {
|
|
116
|
+
// Legacy X- headers
|
|
117
|
+
ammo.res.setHeader('X-RateLimit-Limit', limiter.options.maxRequests);
|
|
118
|
+
ammo.res.setHeader('X-RateLimit-Remaining', result.remainingRequests);
|
|
119
|
+
ammo.res.setHeader('X-RateLimit-Reset', result.resetTime);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Always set Retry-After on 429 responses
|
|
123
|
+
if (!result.success) {
|
|
124
|
+
const retryAfter = result.resetTime - Math.floor(Date.now() / 1000);
|
|
125
|
+
ammo.res.setHeader('Retry-After', retryAfter);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Return middleware function
|
|
130
|
+
return async (ammo, next) => {
|
|
131
|
+
const key = keyGenerator(ammo);
|
|
132
|
+
const result = await limiter.consume(key);
|
|
133
|
+
|
|
134
|
+
setRateLimitHeaders(ammo, result);
|
|
135
|
+
|
|
136
|
+
if (!result.success) {
|
|
137
|
+
if (onRateLimited) {
|
|
138
|
+
return onRateLimited(ammo);
|
|
139
|
+
}
|
|
140
|
+
return ammo.throw(429, 'Too Many Requests');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await next();
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default rateLimiter;
|