@commandkit/ratelimit 0.0.0-dev.20260317060555
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/LICENSE +21 -0
- package/README.md +801 -0
- package/dist/api.d.ts +79 -0
- package/dist/api.js +266 -0
- package/dist/augmentation.d.ts +11 -0
- package/dist/augmentation.js +8 -0
- package/dist/configure.d.ts +28 -0
- package/dist/configure.js +85 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.js +21 -0
- package/dist/directive/use-ratelimit-directive.d.ts +22 -0
- package/dist/directive/use-ratelimit-directive.js +38 -0
- package/dist/directive/use-ratelimit.d.ts +14 -0
- package/dist/directive/use-ratelimit.js +169 -0
- package/dist/engine/RateLimitEngine.d.ts +48 -0
- package/dist/engine/RateLimitEngine.js +137 -0
- package/dist/engine/algorithms/fixed-window.d.ts +44 -0
- package/dist/engine/algorithms/fixed-window.js +198 -0
- package/dist/engine/algorithms/leaky-bucket.d.ts +48 -0
- package/dist/engine/algorithms/leaky-bucket.js +119 -0
- package/dist/engine/algorithms/sliding-window.d.ts +45 -0
- package/dist/engine/algorithms/sliding-window.js +127 -0
- package/dist/engine/algorithms/token-bucket.d.ts +47 -0
- package/dist/engine/algorithms/token-bucket.js +118 -0
- package/dist/engine/violations.d.ts +55 -0
- package/dist/engine/violations.js +106 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.js +28 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +53 -0
- package/dist/plugin.d.ts +140 -0
- package/dist/plugin.js +796 -0
- package/dist/providers/fallback.d.ts +7 -0
- package/dist/providers/fallback.js +11 -0
- package/dist/providers/memory.d.ts +6 -0
- package/dist/providers/memory.js +11 -0
- package/dist/providers/redis.d.ts +7 -0
- package/dist/providers/redis.js +11 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.js +67 -0
- package/dist/storage/fallback.d.ts +180 -0
- package/dist/storage/fallback.js +261 -0
- package/dist/storage/memory.d.ts +146 -0
- package/dist/storage/memory.js +304 -0
- package/dist/storage/redis.d.ts +130 -0
- package/dist/storage/redis.js +243 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +40 -0
- package/dist/utils/config.d.ts +34 -0
- package/dist/utils/config.js +105 -0
- package/dist/utils/keys.d.ts +102 -0
- package/dist/utils/keys.js +304 -0
- package/dist/utils/locking.d.ts +17 -0
- package/dist/utils/locking.js +60 -0
- package/dist/utils/time.d.ts +23 -0
- package/dist/utils/time.js +72 -0
- package/package.json +65 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Runtime wrapper for the "use ratelimit" directive.
|
|
4
|
+
*
|
|
5
|
+
* Uses the runtime default limiter for arbitrary async functions.
|
|
6
|
+
* Throws RateLimitError when the call is limited.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.$ckitirl = void 0;
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const RateLimitEngine_1 = require("../engine/RateLimitEngine");
|
|
12
|
+
const errors_1 = require("../errors");
|
|
13
|
+
const config_1 = require("../utils/config");
|
|
14
|
+
const runtime_1 = require("../runtime");
|
|
15
|
+
const constants_1 = require("../constants");
|
|
16
|
+
const RATELIMIT_FN_SYMBOL = Symbol('commandkit.ratelimit.directive');
|
|
17
|
+
let cachedEngine = null;
|
|
18
|
+
let cachedStorage = null;
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the cached engine instance for a storage backend.
|
|
21
|
+
*
|
|
22
|
+
* @param storage - Storage backend to associate with the engine.
|
|
23
|
+
* @returns Cached engine instance for the storage.
|
|
24
|
+
*/
|
|
25
|
+
function getEngine(storage) {
|
|
26
|
+
/**
|
|
27
|
+
* Cache per storage instance so violation tracking stays consistent.
|
|
28
|
+
*/
|
|
29
|
+
if (!cachedEngine || cachedStorage !== storage) {
|
|
30
|
+
cachedEngine = new RateLimitEngine_1.RateLimitEngine(storage);
|
|
31
|
+
cachedStorage = storage;
|
|
32
|
+
}
|
|
33
|
+
return cachedEngine;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Apply an optional prefix to a storage key.
|
|
37
|
+
*
|
|
38
|
+
* @param prefix - Optional prefix to prepend.
|
|
39
|
+
* @param key - Base key to prefix.
|
|
40
|
+
* @returns Prefixed key.
|
|
41
|
+
*/
|
|
42
|
+
function withPrefix(prefix, key) {
|
|
43
|
+
if (!prefix)
|
|
44
|
+
return key;
|
|
45
|
+
return `${prefix}${key}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Append a window suffix to a key when a window id is present.
|
|
49
|
+
*
|
|
50
|
+
* @param key - Base storage key.
|
|
51
|
+
* @param windowId - Optional window identifier.
|
|
52
|
+
* @returns Key with window suffix when provided.
|
|
53
|
+
*/
|
|
54
|
+
function withWindowSuffix(key, windowId) {
|
|
55
|
+
if (!windowId)
|
|
56
|
+
return key;
|
|
57
|
+
return `${key}:w:${windowId}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Merge a runtime default limiter with an override when provided.
|
|
61
|
+
*
|
|
62
|
+
* @param runtimeDefault - Runtime default limiter configuration.
|
|
63
|
+
* @param limiter - Optional override limiter.
|
|
64
|
+
* @returns Resolved limiter configuration.
|
|
65
|
+
*/
|
|
66
|
+
function resolveLimiter(runtimeDefault, limiter) {
|
|
67
|
+
if (!limiter)
|
|
68
|
+
return runtimeDefault;
|
|
69
|
+
return (0, config_1.mergeLimiterConfigs)(runtimeDefault, limiter);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wrap an async function with the runtime default limiter.
|
|
73
|
+
*
|
|
74
|
+
* Throws RateLimitError when the call exceeds limits.
|
|
75
|
+
*
|
|
76
|
+
* @template R - Argument tuple type for the wrapped async function.
|
|
77
|
+
* @template F - Async function type being wrapped.
|
|
78
|
+
* @param fn - Async function to wrap with rate limiting.
|
|
79
|
+
* @returns Wrapped async function that enforces the default limiter.
|
|
80
|
+
* @throws RateLimitError when the call exceeds limits.
|
|
81
|
+
*/
|
|
82
|
+
function useRateLimit(fn) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(fn, RATELIMIT_FN_SYMBOL)) {
|
|
84
|
+
return fn;
|
|
85
|
+
}
|
|
86
|
+
const fnId = (0, node_crypto_1.randomUUID)();
|
|
87
|
+
const wrapped = (async (...args) => {
|
|
88
|
+
const runtime = (0, runtime_1.getRateLimitRuntime)();
|
|
89
|
+
if (!runtime) {
|
|
90
|
+
throw new Error('RateLimit runtime is not initialized. Register the RateLimitPlugin first.');
|
|
91
|
+
}
|
|
92
|
+
const limiterConfig = resolveLimiter((0, config_1.mergeLimiterConfigs)(config_1.DEFAULT_LIMITER, runtime.defaultLimiter));
|
|
93
|
+
const key = `${constants_1.DEFAULT_KEY_PREFIX}fn:${fnId}`;
|
|
94
|
+
const finalKey = withPrefix(runtime.keyPrefix, key);
|
|
95
|
+
const engine = getEngine(runtime.storage);
|
|
96
|
+
const resolvedConfigs = (0, config_1.resolveLimiterConfigs)(limiterConfig, 'custom');
|
|
97
|
+
const results = [];
|
|
98
|
+
for (const resolved of resolvedConfigs) {
|
|
99
|
+
const resolvedKey = withWindowSuffix(finalKey, resolved.windowId);
|
|
100
|
+
const { result } = await engine.consume(resolvedKey, resolved);
|
|
101
|
+
results.push(result);
|
|
102
|
+
}
|
|
103
|
+
const aggregate = aggregateResults(results);
|
|
104
|
+
if (aggregate.limited) {
|
|
105
|
+
throw new errors_1.RateLimitError(aggregate);
|
|
106
|
+
}
|
|
107
|
+
return fn(...args);
|
|
108
|
+
});
|
|
109
|
+
Object.defineProperty(wrapped, RATELIMIT_FN_SYMBOL, {
|
|
110
|
+
value: true,
|
|
111
|
+
configurable: false,
|
|
112
|
+
enumerable: false,
|
|
113
|
+
writable: false,
|
|
114
|
+
});
|
|
115
|
+
return wrapped;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Aggregate multiple rate-limit results into a single summary object.
|
|
119
|
+
*
|
|
120
|
+
* @param results - Individual limiter/window results.
|
|
121
|
+
* @returns Aggregated rate-limit store value.
|
|
122
|
+
*/
|
|
123
|
+
function aggregateResults(results) {
|
|
124
|
+
if (!results.length) {
|
|
125
|
+
return {
|
|
126
|
+
limited: false,
|
|
127
|
+
remaining: 0,
|
|
128
|
+
resetAt: 0,
|
|
129
|
+
retryAfter: 0,
|
|
130
|
+
results: [],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const limitedResults = results.filter((r) => r.limited);
|
|
134
|
+
const limited = limitedResults.length > 0;
|
|
135
|
+
const remaining = Math.min(...results.map((r) => r.remaining));
|
|
136
|
+
const resetAt = Math.max(...results.map((r) => r.resetAt));
|
|
137
|
+
const retryAfter = limited
|
|
138
|
+
? Math.max(...limitedResults.map((r) => r.retryAfter))
|
|
139
|
+
: 0;
|
|
140
|
+
return {
|
|
141
|
+
limited,
|
|
142
|
+
remaining,
|
|
143
|
+
resetAt,
|
|
144
|
+
retryAfter,
|
|
145
|
+
results,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Wrapper symbol injected by the compiler plugin.
|
|
150
|
+
*
|
|
151
|
+
* @param fn - Generic function to wrap with runtime rate limiting.
|
|
152
|
+
* @returns Wrapped function that enforces the runtime default limiter.
|
|
153
|
+
*/
|
|
154
|
+
const $ckitirl = (fn) => {
|
|
155
|
+
return useRateLimit(fn);
|
|
156
|
+
};
|
|
157
|
+
exports.$ckitirl = $ckitirl;
|
|
158
|
+
if (!('$ckitirl' in globalThis)) {
|
|
159
|
+
/**
|
|
160
|
+
* Expose the wrapper globally so directive transforms can call it.
|
|
161
|
+
*/
|
|
162
|
+
Object.defineProperty(globalThis, '$ckitirl', {
|
|
163
|
+
value: exports.$ckitirl,
|
|
164
|
+
configurable: false,
|
|
165
|
+
enumerable: false,
|
|
166
|
+
writable: false,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNlLXJhdGVsaW1pdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9kaXJlY3RpdmUvdXNlLXJhdGVsaW1pdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7O0dBS0c7OztBQUVILDZDQUF5QztBQUV6QywrREFBNEQ7QUFDNUQsc0NBQTJDO0FBTzNDLDRDQUl5QjtBQUN6Qix3Q0FBaUQ7QUFDakQsNENBQWtEO0FBRWxELE1BQU0sbUJBQW1CLEdBQUcsTUFBTSxDQUFDLGdDQUFnQyxDQUFDLENBQUM7QUFFckUsSUFBSSxZQUFZLEdBQTJCLElBQUksQ0FBQztBQUNoRCxJQUFJLGFBQWEsR0FBNEIsSUFBSSxDQUFDO0FBRWxEOzs7OztHQUtHO0FBQ0gsU0FBUyxTQUFTLENBQUMsT0FBeUI7SUFDMUM7O09BRUc7SUFDSCxJQUFJLENBQUMsWUFBWSxJQUFJLGFBQWEsS0FBSyxPQUFPLEVBQUUsQ0FBQztRQUMvQyxZQUFZLEdBQUcsSUFBSSxpQ0FBZSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FBQzVDLGFBQWEsR0FBRyxPQUFPLENBQUM7SUFDMUIsQ0FBQztJQUNELE9BQU8sWUFBWSxDQUFDO0FBQ3RCLENBQUM7QUFFRDs7Ozs7O0dBTUc7QUFDSCxTQUFTLFVBQVUsQ0FBQyxNQUEwQixFQUFFLEdBQVc7SUFDekQsSUFBSSxDQUFDLE1BQU07UUFBRSxPQUFPLEdBQUcsQ0FBQztJQUN4QixPQUFPLEdBQUcsTUFBTSxHQUFHLEdBQUcsRUFBRSxDQUFDO0FBQzNCLENBQUM7QUFFRDs7Ozs7O0dBTUc7QUFDSCxTQUFTLGdCQUFnQixDQUFDLEdBQVcsRUFBRSxRQUFpQjtJQUN0RCxJQUFJLENBQUMsUUFBUTtRQUFFLE9BQU8sR0FBRyxDQUFDO0lBQzFCLE9BQU8sR0FBRyxHQUFHLE1BQU0sUUFBUSxFQUFFLENBQUM7QUFDaEMsQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILFNBQVMsY0FBYyxDQUNyQixjQUFzQyxFQUN0QyxPQUFnQztJ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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine coordinator.
|
|
3
|
+
*
|
|
4
|
+
* Selects algorithms and applies violation escalation before returning results.
|
|
5
|
+
*/
|
|
6
|
+
import type { RateLimitResult, RateLimitStorage, ResolvedLimiterConfig } from '../types';
|
|
7
|
+
/**
|
|
8
|
+
* Consume output including optional violation count for callers.
|
|
9
|
+
*/
|
|
10
|
+
export interface RateLimitConsumeOutput {
|
|
11
|
+
result: RateLimitResult;
|
|
12
|
+
violationCount?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Coordinates algorithm selection and violation escalation per storage.
|
|
16
|
+
*/
|
|
17
|
+
export declare class RateLimitEngine {
|
|
18
|
+
private readonly storage;
|
|
19
|
+
private readonly violations;
|
|
20
|
+
/**
|
|
21
|
+
* Create a rate limit engine bound to a storage backend.
|
|
22
|
+
*
|
|
23
|
+
* @param storage - Storage backend for rate-limit state.
|
|
24
|
+
*/
|
|
25
|
+
constructor(storage: RateLimitStorage);
|
|
26
|
+
/**
|
|
27
|
+
* Create an algorithm instance for a resolved config.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Resolved limiter configuration.
|
|
30
|
+
* @returns Algorithm instance for the resolved config.
|
|
31
|
+
*/
|
|
32
|
+
private createAlgorithm;
|
|
33
|
+
/**
|
|
34
|
+
* Consume a single key and apply escalation rules when enabled.
|
|
35
|
+
*
|
|
36
|
+
* @param key - Storage key for the limiter.
|
|
37
|
+
* @param config - Resolved limiter configuration.
|
|
38
|
+
* @returns Result plus optional violation count.
|
|
39
|
+
*/
|
|
40
|
+
consume(key: string, config: ResolvedLimiterConfig): Promise<RateLimitConsumeOutput>;
|
|
41
|
+
/**
|
|
42
|
+
* Reset a key and its associated violation state.
|
|
43
|
+
*
|
|
44
|
+
* @param key - Storage key to reset.
|
|
45
|
+
* @returns Resolves after the key and violations are cleared.
|
|
46
|
+
*/
|
|
47
|
+
reset(key: string): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Engine coordinator.
|
|
4
|
+
*
|
|
5
|
+
* Selects algorithms and applies violation escalation before returning results.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RateLimitEngine = void 0;
|
|
9
|
+
const fixed_window_1 = require("./algorithms/fixed-window");
|
|
10
|
+
const sliding_window_1 = require("./algorithms/sliding-window");
|
|
11
|
+
const token_bucket_1 = require("./algorithms/token-bucket");
|
|
12
|
+
const leaky_bucket_1 = require("./algorithms/leaky-bucket");
|
|
13
|
+
const violations_1 = require("./violations");
|
|
14
|
+
/**
|
|
15
|
+
* Coordinates algorithm selection and violation escalation per storage.
|
|
16
|
+
*/
|
|
17
|
+
class RateLimitEngine {
|
|
18
|
+
/**
|
|
19
|
+
* Create a rate limit engine bound to a storage backend.
|
|
20
|
+
*
|
|
21
|
+
* @param storage - Storage backend for rate-limit state.
|
|
22
|
+
*/
|
|
23
|
+
constructor(storage) {
|
|
24
|
+
this.storage = storage;
|
|
25
|
+
this.violations = new violations_1.ViolationTracker(storage);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an algorithm instance for a resolved config.
|
|
29
|
+
*
|
|
30
|
+
* @param config - Resolved limiter configuration.
|
|
31
|
+
* @returns Algorithm instance for the resolved config.
|
|
32
|
+
*/
|
|
33
|
+
createAlgorithm(config) {
|
|
34
|
+
switch (config.algorithm) {
|
|
35
|
+
case 'fixed-window':
|
|
36
|
+
return new fixed_window_1.FixedWindowAlgorithm(this.storage, {
|
|
37
|
+
maxRequests: config.maxRequests,
|
|
38
|
+
intervalMs: config.intervalMs,
|
|
39
|
+
scope: config.scope,
|
|
40
|
+
});
|
|
41
|
+
case 'sliding-window':
|
|
42
|
+
return new sliding_window_1.SlidingWindowLogAlgorithm(this.storage, {
|
|
43
|
+
maxRequests: config.maxRequests,
|
|
44
|
+
intervalMs: config.intervalMs,
|
|
45
|
+
scope: config.scope,
|
|
46
|
+
});
|
|
47
|
+
case 'token-bucket':
|
|
48
|
+
return new token_bucket_1.TokenBucketAlgorithm(this.storage, {
|
|
49
|
+
capacity: config.burst,
|
|
50
|
+
refillRate: config.refillRate,
|
|
51
|
+
scope: config.scope,
|
|
52
|
+
});
|
|
53
|
+
case 'leaky-bucket':
|
|
54
|
+
return new leaky_bucket_1.LeakyBucketAlgorithm(this.storage, {
|
|
55
|
+
capacity: config.burst,
|
|
56
|
+
leakRate: config.leakRate,
|
|
57
|
+
scope: config.scope,
|
|
58
|
+
});
|
|
59
|
+
default:
|
|
60
|
+
/**
|
|
61
|
+
* Fall back to fixed-window so unknown algorithms still enforce a limit.
|
|
62
|
+
*/
|
|
63
|
+
return new fixed_window_1.FixedWindowAlgorithm(this.storage, {
|
|
64
|
+
maxRequests: config.maxRequests,
|
|
65
|
+
intervalMs: config.intervalMs,
|
|
66
|
+
scope: config.scope,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Consume a single key and apply escalation rules when enabled.
|
|
72
|
+
*
|
|
73
|
+
* @param key - Storage key for the limiter.
|
|
74
|
+
* @param config - Resolved limiter configuration.
|
|
75
|
+
* @returns Result plus optional violation count.
|
|
76
|
+
*/
|
|
77
|
+
async consume(key, config) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const shouldEscalate = config.violations != null && config.violations.escalate !== false;
|
|
80
|
+
if (shouldEscalate) {
|
|
81
|
+
const active = await this.violations.checkCooldown(key);
|
|
82
|
+
if (active) {
|
|
83
|
+
/**
|
|
84
|
+
* When an escalation cooldown is active, skip the algorithm to enforce the cooldown.
|
|
85
|
+
*/
|
|
86
|
+
const limit = config.algorithm === 'token-bucket' ||
|
|
87
|
+
config.algorithm === 'leaky-bucket'
|
|
88
|
+
? config.burst
|
|
89
|
+
: config.maxRequests;
|
|
90
|
+
const result = {
|
|
91
|
+
key,
|
|
92
|
+
scope: config.scope,
|
|
93
|
+
algorithm: config.algorithm,
|
|
94
|
+
limited: true,
|
|
95
|
+
remaining: 0,
|
|
96
|
+
resetAt: active.cooldownUntil,
|
|
97
|
+
retryAfter: Math.max(0, active.cooldownUntil - now),
|
|
98
|
+
limit,
|
|
99
|
+
windowId: config.windowId,
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
result,
|
|
103
|
+
violationCount: active.count,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const algorithm = this.createAlgorithm(config);
|
|
108
|
+
const result = await algorithm.consume(key);
|
|
109
|
+
if (config.windowId) {
|
|
110
|
+
result.windowId = config.windowId;
|
|
111
|
+
}
|
|
112
|
+
if (result.limited && shouldEscalate) {
|
|
113
|
+
const state = await this.violations.recordViolation(key, result.retryAfter, config.violations);
|
|
114
|
+
/**
|
|
115
|
+
* If escalation extends the cooldown, update the result so retry info stays accurate.
|
|
116
|
+
*/
|
|
117
|
+
if (state.cooldownUntil > result.resetAt) {
|
|
118
|
+
result.resetAt = state.cooldownUntil;
|
|
119
|
+
result.retryAfter = Math.max(0, state.cooldownUntil - now);
|
|
120
|
+
}
|
|
121
|
+
return { result, violationCount: state.count };
|
|
122
|
+
}
|
|
123
|
+
return { result };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reset a key and its associated violation state.
|
|
127
|
+
*
|
|
128
|
+
* @param key - Storage key to reset.
|
|
129
|
+
* @returns Resolves after the key and violations are cleared.
|
|
130
|
+
*/
|
|
131
|
+
async reset(key) {
|
|
132
|
+
await this.storage.delete(key);
|
|
133
|
+
await this.violations.reset(key);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.RateLimitEngine = RateLimitEngine;
|
|
137
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiUmF0ZUxpbWl0RW5naW5lLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2VuZ2luZS9SYXRlTGltaXRFbmdpbmUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7O0dBSUc7OztBQVNILDREQUFpRTtBQUNqRSxnRUFBd0U7QUFDeEUsNERBQWlFO0FBQ2pFLDREQUFpRTtBQUNqRSw2Q0FBZ0Q7QUFVaEQ7O0dBRUc7QUFDSCxNQUFhLGVBQWU7SUFHMUI7Ozs7T0FJRztJQUNILFlBQW9DLE9BQXlCO1FBQXpCLFlBQU8sR0FBUCxPQUFPLENBQWtCO1FBQzNELElBQUksQ0FBQyxVQUFVLEdBQUcsSUFBSSw2QkFBZ0IsQ0FBQyxPQUFPLENBQUMsQ0FBQztJQUNsRCxDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSyxlQUFlLENBQUMsTUFBNkI7UUFDbkQsUUFBUSxNQUFNLENBQUMsU0FBUyxFQUFFLENBQUM7WUFDekIsS0FBSyxjQUFjO2dCQUNqQixPQUFPLElBQUksbUNBQW9CLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRTtvQkFDNUMsV0FBVyxFQUFFLE1BQU0sQ0FBQyxXQUFXO29CQUMvQixVQUFVLEVBQUUsTUFBTSxDQUFDLFVBQVU7b0JBQzdCLEtBQUssRUFBRSxNQUFNLENBQUMsS0FBSztpQkFDcEIsQ0FBQyxDQUFDO1lBQ0wsS0FBSyxnQkFBZ0I7Z0JBQ25CLE9BQU8sSUFBSSwwQ0FBeUIsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFO29CQUNqRCxXQUFXLEVBQUUsTUFBTSxDQUFDLFdBQVc7b0JBQy9CLFVBQVUsRUFBRSxNQUFNLENBQUMsVUFBVTtvQkFDN0IsS0FBSyxFQUFFLE1BQU0sQ0FBQyxLQUFLO2lCQUNwQixDQUFDLENBQUM7WUFDTCxLQUFLLGNBQWM7Z0JBQ2pCLE9BQU8sSUFBSSxtQ0FBb0IsQ0FBQyxJQUFJLENBQUMsT0FBTyxFQUFFO29CQUM1QyxRQUFRLEVBQUUsTUFBTSxDQUFDLEtBQUs7b0JBQ3RCLFVBQVUs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
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed window rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Simple counters per window are fast and predictable, at the cost of allowing
|
|
5
|
+
* bursts within the window boundary. Prefer atomic storage for correctness.
|
|
6
|
+
*/
|
|
7
|
+
import type { RateLimitAlgorithm, RateLimitAlgorithmType, RateLimitResult, RateLimitStorage } from '../../types';
|
|
8
|
+
interface FixedWindowConfig {
|
|
9
|
+
maxRequests: number;
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
scope: RateLimitResult['scope'];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Basic fixed-window counter for low-cost rate limits.
|
|
15
|
+
*
|
|
16
|
+
* @implements RateLimitAlgorithm
|
|
17
|
+
*/
|
|
18
|
+
export declare class FixedWindowAlgorithm implements RateLimitAlgorithm {
|
|
19
|
+
private readonly storage;
|
|
20
|
+
private readonly config;
|
|
21
|
+
readonly type: RateLimitAlgorithmType;
|
|
22
|
+
/**
|
|
23
|
+
* Create a fixed-window algorithm bound to a storage backend.
|
|
24
|
+
*
|
|
25
|
+
* @param storage - Storage backend for rate-limit state.
|
|
26
|
+
* @param config - Fixed-window configuration.
|
|
27
|
+
*/
|
|
28
|
+
constructor(storage: RateLimitStorage, config: FixedWindowConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Record one attempt and return the current window status for this key.
|
|
31
|
+
*
|
|
32
|
+
* @param key - Storage key for the limiter.
|
|
33
|
+
* @returns Rate limit result for the current window.
|
|
34
|
+
*/
|
|
35
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
36
|
+
/**
|
|
37
|
+
* Reset the stored key state for this limiter.
|
|
38
|
+
*
|
|
39
|
+
* @param key - Storage key to reset.
|
|
40
|
+
* @returns Resolves after the key is deleted.
|
|
41
|
+
*/
|
|
42
|
+
reset(key: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fixed window rate limiting.
|
|
4
|
+
*
|
|
5
|
+
* Simple counters per window are fast and predictable, at the cost of allowing
|
|
6
|
+
* bursts within the window boundary. Prefer atomic storage for correctness.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.FixedWindowAlgorithm = void 0;
|
|
10
|
+
const locking_1 = require("../../utils/locking");
|
|
11
|
+
/**
|
|
12
|
+
* Basic fixed-window counter for low-cost rate limits.
|
|
13
|
+
*
|
|
14
|
+
* @implements RateLimitAlgorithm
|
|
15
|
+
*/
|
|
16
|
+
class FixedWindowAlgorithm {
|
|
17
|
+
/**
|
|
18
|
+
* Create a fixed-window algorithm bound to a storage backend.
|
|
19
|
+
*
|
|
20
|
+
* @param storage - Storage backend for rate-limit state.
|
|
21
|
+
* @param config - Fixed-window configuration.
|
|
22
|
+
*/
|
|
23
|
+
constructor(storage, config) {
|
|
24
|
+
this.storage = storage;
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.type = 'fixed-window';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Record one attempt and return the current window status for this key.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Storage key for the limiter.
|
|
32
|
+
* @returns Rate limit result for the current window.
|
|
33
|
+
*/
|
|
34
|
+
async consume(key) {
|
|
35
|
+
const limit = this.config.maxRequests;
|
|
36
|
+
const interval = this.config.intervalMs;
|
|
37
|
+
if (this.storage.consumeFixedWindow) {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const { count, ttlMs } = await this.storage.consumeFixedWindow(key, limit, interval, now);
|
|
40
|
+
const resetAt = now + ttlMs;
|
|
41
|
+
const limited = count > limit;
|
|
42
|
+
return {
|
|
43
|
+
key,
|
|
44
|
+
scope: this.config.scope,
|
|
45
|
+
algorithm: this.type,
|
|
46
|
+
limited,
|
|
47
|
+
remaining: Math.max(0, limit - count),
|
|
48
|
+
resetAt,
|
|
49
|
+
retryAfter: limited ? Math.max(0, resetAt - now) : 0,
|
|
50
|
+
limit,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (this.storage.incr) {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const { count, ttlMs } = await this.storage.incr(key, interval);
|
|
56
|
+
const resetAt = now + ttlMs;
|
|
57
|
+
const limited = count > limit;
|
|
58
|
+
return {
|
|
59
|
+
key,
|
|
60
|
+
scope: this.config.scope,
|
|
61
|
+
algorithm: this.type,
|
|
62
|
+
limited,
|
|
63
|
+
remaining: Math.max(0, limit - count),
|
|
64
|
+
resetAt,
|
|
65
|
+
retryAfter: limited ? Math.max(0, resetAt - now) : 0,
|
|
66
|
+
limit,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Fallback is serialized per process to avoid same-instance races.
|
|
71
|
+
* Multi-process strictness still requires atomic storage operations.
|
|
72
|
+
*/
|
|
73
|
+
return (0, locking_1.withStorageKeyLock)(this.storage, key, async () => {
|
|
74
|
+
const maxRetries = 5;
|
|
75
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
76
|
+
const attemptNow = Date.now();
|
|
77
|
+
const existingRaw = await this.storage.get(key);
|
|
78
|
+
const existing = isFixedWindowState(existingRaw) ? existingRaw : null;
|
|
79
|
+
if (!existing || existing.resetAt <= attemptNow) {
|
|
80
|
+
const resetAt = attemptNow + interval;
|
|
81
|
+
const state = { count: 1, resetAt, version: 1 };
|
|
82
|
+
const currentRaw = await this.storage.get(key);
|
|
83
|
+
const current = isFixedWindowState(currentRaw) ? currentRaw : null;
|
|
84
|
+
if (current && current.resetAt > attemptNow) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
await this.storage.set(key, state, interval);
|
|
88
|
+
const verifyRaw = await this.storage.get(key);
|
|
89
|
+
const verify = isFixedWindowState(verifyRaw) ? verifyRaw : null;
|
|
90
|
+
if ((verify?.version ?? 0) !== 1) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
key,
|
|
95
|
+
scope: this.config.scope,
|
|
96
|
+
algorithm: this.type,
|
|
97
|
+
limited: false,
|
|
98
|
+
remaining: Math.max(0, limit - 1),
|
|
99
|
+
resetAt,
|
|
100
|
+
retryAfter: 0,
|
|
101
|
+
limit,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (existing.count >= limit) {
|
|
105
|
+
return {
|
|
106
|
+
key,
|
|
107
|
+
scope: this.config.scope,
|
|
108
|
+
algorithm: this.type,
|
|
109
|
+
limited: true,
|
|
110
|
+
remaining: 0,
|
|
111
|
+
resetAt: existing.resetAt,
|
|
112
|
+
retryAfter: Math.max(0, existing.resetAt - attemptNow),
|
|
113
|
+
limit,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
let nextState = {
|
|
117
|
+
count: existing.count + 1,
|
|
118
|
+
resetAt: existing.resetAt,
|
|
119
|
+
version: (existing.version ?? 0) + 1,
|
|
120
|
+
};
|
|
121
|
+
const currentRaw = await this.storage.get(key);
|
|
122
|
+
const current = isFixedWindowState(currentRaw) ? currentRaw : null;
|
|
123
|
+
if (!current ||
|
|
124
|
+
current.resetAt !== existing.resetAt ||
|
|
125
|
+
current.count !== existing.count ||
|
|
126
|
+
(current.version ?? 0) !== (existing.version ?? 0)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let ttlMs = existing.resetAt - attemptNow;
|
|
130
|
+
if (ttlMs <= 0) {
|
|
131
|
+
nextState = {
|
|
132
|
+
count: 1,
|
|
133
|
+
resetAt: attemptNow + interval,
|
|
134
|
+
version: 1,
|
|
135
|
+
};
|
|
136
|
+
ttlMs = interval;
|
|
137
|
+
}
|
|
138
|
+
await this.storage.set(key, nextState, ttlMs);
|
|
139
|
+
const verifyRaw = await this.storage.get(key);
|
|
140
|
+
const verify = isFixedWindowState(verifyRaw) ? verifyRaw : null;
|
|
141
|
+
if ((verify?.version ?? 0) !== (nextState.version ?? 0)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
key,
|
|
146
|
+
scope: this.config.scope,
|
|
147
|
+
algorithm: this.type,
|
|
148
|
+
limited: false,
|
|
149
|
+
remaining: Math.max(0, limit - nextState.count),
|
|
150
|
+
resetAt: nextState.resetAt,
|
|
151
|
+
retryAfter: 0,
|
|
152
|
+
limit,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const resetAt = now + interval;
|
|
157
|
+
return {
|
|
158
|
+
key,
|
|
159
|
+
scope: this.config.scope,
|
|
160
|
+
algorithm: this.type,
|
|
161
|
+
limited: true,
|
|
162
|
+
remaining: 0,
|
|
163
|
+
resetAt,
|
|
164
|
+
retryAfter: Math.max(0, resetAt - now),
|
|
165
|
+
limit,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Reset the stored key state for this limiter.
|
|
171
|
+
*
|
|
172
|
+
* @param key - Storage key to reset.
|
|
173
|
+
* @returns Resolves after the key is deleted.
|
|
174
|
+
*/
|
|
175
|
+
async reset(key) {
|
|
176
|
+
await this.storage.delete(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
exports.FixedWindowAlgorithm = FixedWindowAlgorithm;
|
|
180
|
+
/**
|
|
181
|
+
* Type guard for fixed-window state entries loaded from storage.
|
|
182
|
+
*
|
|
183
|
+
* @param value - Stored value to validate.
|
|
184
|
+
* @returns True when the value matches the FixedWindowState shape.
|
|
185
|
+
*/
|
|
186
|
+
function isFixedWindowState(value) {
|
|
187
|
+
if (!value || typeof value !== 'object')
|
|
188
|
+
return false;
|
|
189
|
+
const state = value;
|
|
190
|
+
const hasValidVersion = state.version === undefined ||
|
|
191
|
+
(typeof state.version === 'number' && Number.isFinite(state.version));
|
|
192
|
+
return (typeof state.count === 'number' &&
|
|
193
|
+
Number.isFinite(state.count) &&
|
|
194
|
+
typeof state.resetAt === 'number' &&
|
|
195
|
+
Number.isFinite(state.resetAt) &&
|
|
196
|
+
hasValidVersion);
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZml4ZWQtd2luZG93LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2VuZ2luZS9hbGdvcml0aG1zL2ZpeGVkLXdpbmRvdy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7O0dBS0c7OztBQVFILGlEQUF5RDtBQWN6RDs7OztHQUlHO0FBQ0gsTUFBYSxvQkFBb0I7SUFHL0I7Ozs7O09BS0c7SUFDSCxZQUNtQixPQUF5QixFQUN6QixNQUF5QjtRQUR6QixZQUFPLEdBQVAsT0FBTyxDQUFrQjtRQUN6QixXQUFNLEdBQU4sTUFBTSxDQUFtQjtRQVY1QixTQUFJLEdBQTJCLGNBQWMsQ0FBQztJQVczRCxDQUFDO0lBRUo7Ozs7O09BS0c7SUFDSSxLQUFLLENBQUMsT0FBTyxDQUFDLEdBQVc7UUFDOUIsTUFBTSxLQUFLLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxXQUFXLENBQUM7UUFDdEMsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxVQUFVLENBQUM7UUFFeEMsSUFBSSxJQUFJLENBQUMsT0FBTyxDQUFDLGtCQUFrQixFQUFFLENBQUM7WUFDcEMsTUFBTSxHQUFHLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDO1lBQ3ZCLE1BQU0sRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLGtCQUFrQixDQUM1RCxHQUFHLEVBQ0gsS0FBSyxFQUNMLFFBQVEsRUFDUixHQUFHLENBQ0osQ0FBQztZQUNGLE1BQU0sT0FBTyxHQUFHLEdBQUcsR0FBRyxLQUFLLENBQUM7WUFDNUIsTUFBTSxPQUFPLEdBQUcsS0FBSyxHQUFHLEtBQUssQ0FBQztZQUM5QixPQUFPO2dCQUNMLEdBQUc7Z0JBQ0gsS0FBSyxFQUFFLElBQUksQ0FBQyxNQUFNLENBQUMsS0FBSztnQkFDeEIsU0FBUyxFQUFFLElBQUksQ0FBQyxJQUFJO2dCQUNwQixPQUFPO2dCQUNQLFNBQVMsRUFB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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaky bucket rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Drains at a steady rate to smooth spikes in traffic.
|
|
5
|
+
* The stored level keeps limits consistent across commands.
|
|
6
|
+
*/
|
|
7
|
+
import type { RateLimitAlgorithm, RateLimitAlgorithmType, RateLimitResult, RateLimitStorage } from '../../types';
|
|
8
|
+
interface LeakyBucketConfig {
|
|
9
|
+
/** Maximum fill level before limiting. */
|
|
10
|
+
capacity: number;
|
|
11
|
+
/** Tokens drained per second during leak. */
|
|
12
|
+
leakRate: number;
|
|
13
|
+
/** Scope reported in rate-limit results. */
|
|
14
|
+
scope: RateLimitResult['scope'];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Leaky bucket algorithm for smoothing output to a steady rate.
|
|
18
|
+
*
|
|
19
|
+
* @implements RateLimitAlgorithm
|
|
20
|
+
*/
|
|
21
|
+
export declare class LeakyBucketAlgorithm implements RateLimitAlgorithm {
|
|
22
|
+
private readonly storage;
|
|
23
|
+
private readonly config;
|
|
24
|
+
readonly type: RateLimitAlgorithmType;
|
|
25
|
+
/**
|
|
26
|
+
* Create a leaky-bucket algorithm bound to a storage backend.
|
|
27
|
+
*
|
|
28
|
+
* @param storage - Storage backend for rate-limit state.
|
|
29
|
+
* @param config - Leaky-bucket configuration.
|
|
30
|
+
*/
|
|
31
|
+
constructor(storage: RateLimitStorage, config: LeakyBucketConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Record one attempt and return the current bucket status for this key.
|
|
34
|
+
*
|
|
35
|
+
* @param key - Storage key for the limiter.
|
|
36
|
+
* @returns Rate limit result for the current bucket.
|
|
37
|
+
* @throws Error when leakRate is non-positive.
|
|
38
|
+
*/
|
|
39
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Reset the stored key state for this limiter.
|
|
42
|
+
*
|
|
43
|
+
* @param key - Storage key to reset.
|
|
44
|
+
* @returns Resolves after the key is deleted.
|
|
45
|
+
*/
|
|
46
|
+
reset(key: string): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export {};
|