@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,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Leaky bucket rate limiting.
|
|
4
|
+
*
|
|
5
|
+
* Drains at a steady rate to smooth spikes in traffic.
|
|
6
|
+
* The stored level keeps limits consistent across commands.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.LeakyBucketAlgorithm = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Leaky bucket algorithm for smoothing output to a steady rate.
|
|
12
|
+
*
|
|
13
|
+
* @implements RateLimitAlgorithm
|
|
14
|
+
*/
|
|
15
|
+
class LeakyBucketAlgorithm {
|
|
16
|
+
/**
|
|
17
|
+
* Create a leaky-bucket algorithm bound to a storage backend.
|
|
18
|
+
*
|
|
19
|
+
* @param storage - Storage backend for rate-limit state.
|
|
20
|
+
* @param config - Leaky-bucket configuration.
|
|
21
|
+
*/
|
|
22
|
+
constructor(storage, config) {
|
|
23
|
+
this.storage = storage;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.type = 'leaky-bucket';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record one attempt and return the current bucket status for this key.
|
|
29
|
+
*
|
|
30
|
+
* @param key - Storage key for the limiter.
|
|
31
|
+
* @returns Rate limit result for the current bucket.
|
|
32
|
+
* @throws Error when leakRate is non-positive.
|
|
33
|
+
*/
|
|
34
|
+
async consume(key) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const { capacity, leakRate } = this.config;
|
|
37
|
+
if (leakRate <= 0) {
|
|
38
|
+
throw new Error('leakRate must be greater than 0');
|
|
39
|
+
}
|
|
40
|
+
const stored = await this.storage.get(key);
|
|
41
|
+
const state = isLeakyBucketState(stored)
|
|
42
|
+
? stored
|
|
43
|
+
: { level: 0, lastLeak: now };
|
|
44
|
+
const elapsedSeconds = Math.max(0, (now - state.lastLeak) / 1000);
|
|
45
|
+
const leaked = Math.max(0, state.level - elapsedSeconds * leakRate);
|
|
46
|
+
const nextState = {
|
|
47
|
+
level: leaked,
|
|
48
|
+
lastLeak: now,
|
|
49
|
+
};
|
|
50
|
+
if (leaked + 1 > capacity) {
|
|
51
|
+
const overflow = leaked + 1 - capacity;
|
|
52
|
+
const retryAfter = Math.ceil((overflow / leakRate) * 1000);
|
|
53
|
+
const resetAt = now + retryAfter;
|
|
54
|
+
await this.storage.set(key, nextState, estimateLeakyTtl(capacity, leakRate));
|
|
55
|
+
return {
|
|
56
|
+
key,
|
|
57
|
+
scope: this.config.scope,
|
|
58
|
+
algorithm: this.type,
|
|
59
|
+
limited: true,
|
|
60
|
+
remaining: 0,
|
|
61
|
+
resetAt,
|
|
62
|
+
retryAfter,
|
|
63
|
+
limit: capacity,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
nextState.level = leaked + 1;
|
|
67
|
+
await this.storage.set(key, nextState, estimateLeakyTtl(capacity, leakRate));
|
|
68
|
+
const remaining = Math.floor(Math.max(0, capacity - nextState.level));
|
|
69
|
+
const resetAt = now + Math.ceil((nextState.level / leakRate) * 1000);
|
|
70
|
+
return {
|
|
71
|
+
key,
|
|
72
|
+
scope: this.config.scope,
|
|
73
|
+
algorithm: this.type,
|
|
74
|
+
limited: false,
|
|
75
|
+
remaining,
|
|
76
|
+
resetAt,
|
|
77
|
+
retryAfter: 0,
|
|
78
|
+
limit: capacity,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Reset the stored key state for this limiter.
|
|
83
|
+
*
|
|
84
|
+
* @param key - Storage key to reset.
|
|
85
|
+
* @returns Resolves after the key is deleted.
|
|
86
|
+
*/
|
|
87
|
+
async reset(key) {
|
|
88
|
+
await this.storage.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.LeakyBucketAlgorithm = LeakyBucketAlgorithm;
|
|
92
|
+
/**
|
|
93
|
+
* Type guard for leaky-bucket state entries loaded from storage.
|
|
94
|
+
*
|
|
95
|
+
* @param value - Stored value to validate.
|
|
96
|
+
* @returns True when the value matches the LeakyBucketState shape.
|
|
97
|
+
*/
|
|
98
|
+
function isLeakyBucketState(value) {
|
|
99
|
+
if (!value || typeof value !== 'object')
|
|
100
|
+
return false;
|
|
101
|
+
const state = value;
|
|
102
|
+
return (typeof state.level === 'number' &&
|
|
103
|
+
Number.isFinite(state.level) &&
|
|
104
|
+
typeof state.lastLeak === 'number' &&
|
|
105
|
+
Number.isFinite(state.lastLeak));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Estimate a TTL window large enough to cover full bucket drainage.
|
|
109
|
+
*
|
|
110
|
+
* @param capacity - Bucket capacity.
|
|
111
|
+
* @param leakRate - Tokens drained per second.
|
|
112
|
+
* @returns TTL in milliseconds.
|
|
113
|
+
*/
|
|
114
|
+
function estimateLeakyTtl(capacity, leakRate) {
|
|
115
|
+
if (leakRate <= 0)
|
|
116
|
+
return 60000;
|
|
117
|
+
return Math.ceil((capacity / leakRate) * 1000 * 2);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGVha3ktYnVja2V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2VuZ2luZS9hbGdvcml0aG1zL2xlYWt5LWJ1Y2tldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7O0dBS0c7OztBQXVCSDs7OztHQUlHO0FBQ0gsTUFBYSxvQkFBb0I7SUFHL0I7Ozs7O09BS0c7SUFDSCxZQUNtQixPQUF5QixFQUN6QixNQUF5QjtRQUR6QixZQUFPLEdBQVAsT0FBTyxDQUFrQjtRQUN6QixXQUFNLEdBQU4sTUFBTSxDQUFtQjtRQVY1QixTQUFJLEdBQTJCLGNBQWMsQ0FBQztJQVczRCxDQUFDO0lBRUo7Ozs7OztPQU1HO0lBQ0ksS0FBSyxDQUFDLE9BQU8sQ0FBQyxHQUFXO1FBQzlCLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUN2QixNQUFNLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUM7UUFFM0MsSUFBSSxRQUFRLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDbEIsTUFBTSxJQUFJLEtBQUssQ0FBQyxpQ0FBaUMsQ0FBQyxDQUFDO1FBQ3JELENBQUM7UUFFRCxNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFtQixHQUFHLENBQUMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxrQkFBa0IsQ0FBQyxNQUFNLENBQUM7WUFDdEMsQ0FBQyxDQUFDLE1BQU07WUFDUixDQUFDLENBQUUsRUFBRSxLQUFLLEVBQUUsQ0FBQyxFQUFFLFFBQVEsRUFBRSxHQUFHLEVBQThCLENBQUM7UUFFN0QsTUFBTSxjQUFjLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxHQUFHLEdBQUcsS0FBSyxDQUFDLFFBQVEsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO1FBQ2xFLE1BQU0sTUFBTSxHQUFHLElBQUksQ0FB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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sliding window log rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Tracks individual request timestamps for smoother limits and accurate retry
|
|
5
|
+
* timing. Requires sorted-set support or an atomic storage helper.
|
|
6
|
+
*/
|
|
7
|
+
import type { RateLimitAlgorithm, RateLimitAlgorithmType, RateLimitResult, RateLimitStorage } from '../../types';
|
|
8
|
+
interface SlidingWindowConfig {
|
|
9
|
+
maxRequests: number;
|
|
10
|
+
intervalMs: number;
|
|
11
|
+
scope: RateLimitResult['scope'];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Sliding-window log algorithm for smoother limits.
|
|
15
|
+
*
|
|
16
|
+
* @implements RateLimitAlgorithm
|
|
17
|
+
*/
|
|
18
|
+
export declare class SlidingWindowLogAlgorithm implements RateLimitAlgorithm {
|
|
19
|
+
private readonly storage;
|
|
20
|
+
private readonly config;
|
|
21
|
+
readonly type: RateLimitAlgorithmType;
|
|
22
|
+
/**
|
|
23
|
+
* Create a sliding-window algorithm bound to a storage backend.
|
|
24
|
+
*
|
|
25
|
+
* @param storage - Storage backend for rate-limit state.
|
|
26
|
+
* @param config - Sliding-window configuration.
|
|
27
|
+
*/
|
|
28
|
+
constructor(storage: RateLimitStorage, config: SlidingWindowConfig);
|
|
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
|
+
* @throws Error when the storage backend lacks sorted-set support.
|
|
35
|
+
*/
|
|
36
|
+
consume(key: string): Promise<RateLimitResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Reset the stored key state for this limiter.
|
|
39
|
+
*
|
|
40
|
+
* @param key - Storage key to reset.
|
|
41
|
+
* @returns Resolves after the key is deleted.
|
|
42
|
+
*/
|
|
43
|
+
reset(key: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Sliding window log rate limiting.
|
|
4
|
+
*
|
|
5
|
+
* Tracks individual request timestamps for smoother limits and accurate retry
|
|
6
|
+
* timing. Requires sorted-set support or an atomic storage helper.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SlidingWindowLogAlgorithm = void 0;
|
|
10
|
+
const locking_1 = require("../../utils/locking");
|
|
11
|
+
/**
|
|
12
|
+
* Sliding-window log algorithm for smoother limits.
|
|
13
|
+
*
|
|
14
|
+
* @implements RateLimitAlgorithm
|
|
15
|
+
*/
|
|
16
|
+
class SlidingWindowLogAlgorithm {
|
|
17
|
+
/**
|
|
18
|
+
* Create a sliding-window algorithm bound to a storage backend.
|
|
19
|
+
*
|
|
20
|
+
* @param storage - Storage backend for rate-limit state.
|
|
21
|
+
* @param config - Sliding-window configuration.
|
|
22
|
+
*/
|
|
23
|
+
constructor(storage, config) {
|
|
24
|
+
this.storage = storage;
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.type = 'sliding-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
|
+
* @throws Error when the storage backend lacks sorted-set support.
|
|
34
|
+
*/
|
|
35
|
+
async consume(key) {
|
|
36
|
+
const limit = this.config.maxRequests;
|
|
37
|
+
const windowMs = this.config.intervalMs;
|
|
38
|
+
if (this.storage.consumeSlidingWindowLog) {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
/**
|
|
41
|
+
* Include the timestamp so reset time can be derived without extra reads.
|
|
42
|
+
*/
|
|
43
|
+
const member = `${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
44
|
+
const res = await this.storage.consumeSlidingWindowLog(key, limit, windowMs, now, member);
|
|
45
|
+
const limited = !res.allowed;
|
|
46
|
+
return {
|
|
47
|
+
key,
|
|
48
|
+
scope: this.config.scope,
|
|
49
|
+
algorithm: this.type,
|
|
50
|
+
limited,
|
|
51
|
+
remaining: Math.max(0, limit - res.count),
|
|
52
|
+
resetAt: res.resetAt,
|
|
53
|
+
retryAfter: limited ? Math.max(0, res.resetAt - now) : 0,
|
|
54
|
+
limit,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!this.storage.zRemRangeByScore ||
|
|
58
|
+
!this.storage.zCard ||
|
|
59
|
+
!this.storage.zAdd) {
|
|
60
|
+
throw new Error('Sliding window requires sorted set support in storage');
|
|
61
|
+
}
|
|
62
|
+
return (0, locking_1.withStorageKeyLock)(this.storage, key, async () => {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
/**
|
|
65
|
+
* Include the timestamp so reset time can be derived without extra reads.
|
|
66
|
+
*/
|
|
67
|
+
const member = `${now}-${Math.random().toString(36).slice(2, 8)}`;
|
|
68
|
+
/**
|
|
69
|
+
* Fallback is serialized per process; multi-process strictness needs atomic storage.
|
|
70
|
+
*/
|
|
71
|
+
await this.storage.zRemRangeByScore(key, 0, now - windowMs);
|
|
72
|
+
const count = await this.storage.zCard(key);
|
|
73
|
+
if (count >= limit) {
|
|
74
|
+
const oldestMembers = this.storage.zRangeByScore
|
|
75
|
+
? await this.storage.zRangeByScore(key, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY)
|
|
76
|
+
: [];
|
|
77
|
+
const oldestMember = oldestMembers[0];
|
|
78
|
+
const oldestTs = oldestMember
|
|
79
|
+
? Number(oldestMember.split('-')[0])
|
|
80
|
+
: now;
|
|
81
|
+
const resetAt = oldestTs + windowMs;
|
|
82
|
+
return {
|
|
83
|
+
key,
|
|
84
|
+
scope: this.config.scope,
|
|
85
|
+
algorithm: this.type,
|
|
86
|
+
limited: true,
|
|
87
|
+
remaining: 0,
|
|
88
|
+
resetAt,
|
|
89
|
+
retryAfter: Math.max(0, resetAt - now),
|
|
90
|
+
limit,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
await this.storage.zAdd(key, now, member);
|
|
94
|
+
if (this.storage.expire) {
|
|
95
|
+
await this.storage.expire(key, windowMs);
|
|
96
|
+
}
|
|
97
|
+
const newCount = count + 1;
|
|
98
|
+
const oldestMembers = this.storage.zRangeByScore
|
|
99
|
+
? await this.storage.zRangeByScore(key, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY)
|
|
100
|
+
: [];
|
|
101
|
+
const oldestMember = oldestMembers[0];
|
|
102
|
+
const oldestTs = oldestMember ? Number(oldestMember.split('-')[0]) : now;
|
|
103
|
+
const resetAt = oldestTs + windowMs;
|
|
104
|
+
return {
|
|
105
|
+
key,
|
|
106
|
+
scope: this.config.scope,
|
|
107
|
+
algorithm: this.type,
|
|
108
|
+
limited: false,
|
|
109
|
+
remaining: Math.max(0, limit - newCount),
|
|
110
|
+
resetAt,
|
|
111
|
+
retryAfter: 0,
|
|
112
|
+
limit,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reset the stored key state for this limiter.
|
|
118
|
+
*
|
|
119
|
+
* @param key - Storage key to reset.
|
|
120
|
+
* @returns Resolves after the key is deleted.
|
|
121
|
+
*/
|
|
122
|
+
async reset(key) {
|
|
123
|
+
await this.storage.delete(key);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.SlidingWindowLogAlgorithm = SlidingWindowLogAlgorithm;
|
|
127
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xpZGluZy13aW5kb3cuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZW5naW5lL2FsZ29yaXRobXMvc2xpZGluZy13aW5kb3cudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7OztHQUtHOzs7QUFRSCxpREFBeUQ7QUFRekQ7Ozs7R0FJRztBQUNILE1BQWEseUJBQXlCO0lBR3BDOzs7OztPQUtHO0lBQ0gsWUFDbUIsT0FBeUIsRUFDekIsTUFBMkI7UUFEM0IsWUFBTyxHQUFQLE9BQU8sQ0FBa0I7UUFDekIsV0FBTSxHQUFOLE1BQU0sQ0FBcUI7UUFWOUIsU0FBSSxHQUEyQixnQkFBZ0IsQ0FBQztJQVc3RCxDQUFDO0lBRUo7Ozs7OztPQU1HO0lBQ0ksS0FBSyxDQUFDLE9BQU8sQ0FBQyxHQUFXO1FBQzlCLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDO1FBQ3RDLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDO1FBRXhDLElBQUksSUFBSSxDQUFDLE9BQU8sQ0FBQyx1QkFBdUIsRUFBRSxDQUFDO1lBQ3pDLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztZQUN2Qjs7ZUFFRztZQUNILE1BQU0sTUFBTSxHQUFHLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsRUFBRSxDQUFDO1lBQ2xFLE1BQU0sR0FBRyxHQUFHLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyx1QkFBdUIsQ0FDcEQsR0FBRyxFQUNILEtBQUssRUFDTCxRQUFRLEVBQ1IsR0FBRyxFQUNILE1BQU0sQ0FDUCxDQUFDO1lBQ0YsTUFBTSxPQUFPLEdBQUcsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDO1lBQzdCLE9BQU87Z0JBQ0wsR0FBRztn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
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiting.
|
|
3
|
+
*
|
|
4
|
+
* Allows short bursts while refilling steadily up to a cap.
|
|
5
|
+
* Bucket state is stored so limits stay consistent across commands.
|
|
6
|
+
*/
|
|
7
|
+
import type { RateLimitAlgorithm, RateLimitAlgorithmType, RateLimitResult, RateLimitStorage } from '../../types';
|
|
8
|
+
export interface TokenBucketConfig {
|
|
9
|
+
/** Maximum tokens available when the bucket is full. */
|
|
10
|
+
capacity: number;
|
|
11
|
+
/** Tokens added per second during refill. */
|
|
12
|
+
refillRate: number;
|
|
13
|
+
/** Scope reported in rate-limit results. */
|
|
14
|
+
scope: RateLimitResult['scope'];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Token bucket algorithm for bursty traffic with steady refill.
|
|
18
|
+
*
|
|
19
|
+
* @implements RateLimitAlgorithm
|
|
20
|
+
*/
|
|
21
|
+
export declare class TokenBucketAlgorithm implements RateLimitAlgorithm {
|
|
22
|
+
private readonly storage;
|
|
23
|
+
private readonly config;
|
|
24
|
+
readonly type: RateLimitAlgorithmType;
|
|
25
|
+
/**
|
|
26
|
+
* Create a token-bucket algorithm bound to a storage backend.
|
|
27
|
+
*
|
|
28
|
+
* @param storage - Storage backend for rate-limit state.
|
|
29
|
+
* @param config - Token-bucket configuration.
|
|
30
|
+
*/
|
|
31
|
+
constructor(storage: RateLimitStorage, config: TokenBucketConfig);
|
|
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 refillRate 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
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Token bucket rate limiting.
|
|
4
|
+
*
|
|
5
|
+
* Allows short bursts while refilling steadily up to a cap.
|
|
6
|
+
* Bucket state is stored so limits stay consistent across commands.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.TokenBucketAlgorithm = void 0;
|
|
10
|
+
/**
|
|
11
|
+
* Token bucket algorithm for bursty traffic with steady refill.
|
|
12
|
+
*
|
|
13
|
+
* @implements RateLimitAlgorithm
|
|
14
|
+
*/
|
|
15
|
+
class TokenBucketAlgorithm {
|
|
16
|
+
/**
|
|
17
|
+
* Create a token-bucket algorithm bound to a storage backend.
|
|
18
|
+
*
|
|
19
|
+
* @param storage - Storage backend for rate-limit state.
|
|
20
|
+
* @param config - Token-bucket configuration.
|
|
21
|
+
*/
|
|
22
|
+
constructor(storage, config) {
|
|
23
|
+
this.storage = storage;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.type = 'token-bucket';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Record one attempt and return the current bucket status for this key.
|
|
29
|
+
*
|
|
30
|
+
* @param key - Storage key for the limiter.
|
|
31
|
+
* @returns Rate limit result for the current bucket.
|
|
32
|
+
* @throws Error when refillRate is non-positive.
|
|
33
|
+
*/
|
|
34
|
+
async consume(key) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
const { capacity, refillRate } = this.config;
|
|
37
|
+
if (refillRate <= 0) {
|
|
38
|
+
throw new Error('refillRate must be greater than 0');
|
|
39
|
+
}
|
|
40
|
+
const stored = await this.storage.get(key);
|
|
41
|
+
const state = isTokenBucketState(stored)
|
|
42
|
+
? stored
|
|
43
|
+
: { tokens: capacity, lastRefill: now };
|
|
44
|
+
const elapsedSeconds = Math.max(0, (now - state.lastRefill) / 1000);
|
|
45
|
+
const refilled = Math.min(capacity, state.tokens + elapsedSeconds * refillRate);
|
|
46
|
+
const nextState = {
|
|
47
|
+
tokens: refilled,
|
|
48
|
+
lastRefill: now,
|
|
49
|
+
};
|
|
50
|
+
if (refilled < 1) {
|
|
51
|
+
const retryAfter = Math.ceil(((1 - refilled) / refillRate) * 1000);
|
|
52
|
+
const resetAt = now + retryAfter;
|
|
53
|
+
await this.storage.set(key, nextState, estimateBucketTtl(capacity, refillRate));
|
|
54
|
+
return {
|
|
55
|
+
key,
|
|
56
|
+
scope: this.config.scope,
|
|
57
|
+
algorithm: this.type,
|
|
58
|
+
limited: true,
|
|
59
|
+
remaining: 0,
|
|
60
|
+
resetAt,
|
|
61
|
+
retryAfter,
|
|
62
|
+
limit: capacity,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
nextState.tokens = refilled - 1;
|
|
66
|
+
await this.storage.set(key, nextState, estimateBucketTtl(capacity, refillRate));
|
|
67
|
+
const remaining = Math.floor(nextState.tokens);
|
|
68
|
+
const resetAt = now + Math.ceil(((capacity - nextState.tokens) / refillRate) * 1000);
|
|
69
|
+
return {
|
|
70
|
+
key,
|
|
71
|
+
scope: this.config.scope,
|
|
72
|
+
algorithm: this.type,
|
|
73
|
+
limited: false,
|
|
74
|
+
remaining,
|
|
75
|
+
resetAt,
|
|
76
|
+
retryAfter: 0,
|
|
77
|
+
limit: capacity,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reset the stored key state for this limiter.
|
|
82
|
+
*
|
|
83
|
+
* @param key - Storage key to reset.
|
|
84
|
+
* @returns Resolves after the key is deleted.
|
|
85
|
+
*/
|
|
86
|
+
async reset(key) {
|
|
87
|
+
await this.storage.delete(key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.TokenBucketAlgorithm = TokenBucketAlgorithm;
|
|
91
|
+
/**
|
|
92
|
+
* Type guard for token-bucket state entries loaded from storage.
|
|
93
|
+
*
|
|
94
|
+
* @param value - Stored value to validate.
|
|
95
|
+
* @returns True when the value matches the TokenBucketState shape.
|
|
96
|
+
*/
|
|
97
|
+
function isTokenBucketState(value) {
|
|
98
|
+
if (!value || typeof value !== 'object')
|
|
99
|
+
return false;
|
|
100
|
+
const state = value;
|
|
101
|
+
return (typeof state.tokens === 'number' &&
|
|
102
|
+
Number.isFinite(state.tokens) &&
|
|
103
|
+
typeof state.lastRefill === 'number' &&
|
|
104
|
+
Number.isFinite(state.lastRefill));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Estimate a TTL window large enough to cover full bucket refills.
|
|
108
|
+
*
|
|
109
|
+
* @param capacity - Bucket capacity.
|
|
110
|
+
* @param refillRate - Tokens refilled per second.
|
|
111
|
+
* @returns TTL in milliseconds.
|
|
112
|
+
*/
|
|
113
|
+
function estimateBucketTtl(capacity, refillRate) {
|
|
114
|
+
if (refillRate <= 0)
|
|
115
|
+
return 60000;
|
|
116
|
+
return Math.ceil((capacity / refillRate) * 1000 * 2);
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9rZW4tYnVja2V0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2VuZ2luZS9hbGdvcml0aG1zL3Rva2VuLWJ1Y2tldC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7O0dBS0c7OztBQXVCSDs7OztHQUlHO0FBQ0gsTUFBYSxvQkFBb0I7SUFHL0I7Ozs7O09BS0c7SUFDSCxZQUNtQixPQUF5QixFQUN6QixNQUF5QjtRQUR6QixZQUFPLEdBQVAsT0FBTyxDQUFrQjtRQUN6QixXQUFNLEdBQU4sTUFBTSxDQUFtQjtRQVY1QixTQUFJLEdBQTJCLGNBQWMsQ0FBQztJQVczRCxDQUFDO0lBRUo7Ozs7OztPQU1HO0lBQ0ksS0FBSyxDQUFDLE9BQU8sQ0FBQyxHQUFXO1FBQzlCLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUN2QixNQUFNLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLElBQUksQ0FBQyxNQUFNLENBQUM7UUFFN0MsSUFBSSxVQUFVLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDcEIsTUFBTSxJQUFJLEtBQUssQ0FBQyxtQ0FBbUMsQ0FBQyxDQUFDO1FBQ3ZELENBQUM7UUFFRCxNQUFNLE1BQU0sR0FBRyxNQUFNLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFtQixHQUFHLENBQUMsQ0FBQztRQUM3RCxNQUFNLEtBQUssR0FBRyxrQkFBa0IsQ0FBQyxNQUFNLENBQUM7WUFDdEMsQ0FBQyxDQUFDLE1BQU07WUFDUixDQUFDLENBQUUsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLFVBQVUsRUFBRSxHQUFHLEVBQThCLENBQUM7UUFFdkUsTUFBTSxjQUFjLEdBQUcsSUFBSSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxHQUFHLEdBQUcsS0FBSyxDQUFDLFVBQVUsQ0FBQyxHQUFHLElBQUksQ0FBQyxDQUFDO1FBQ3BFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FB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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Violation tracking.
|
|
3
|
+
*
|
|
4
|
+
* Persists repeat violations so cooldowns can escalate predictably.
|
|
5
|
+
*/
|
|
6
|
+
import type { RateLimitStorage, ViolationOptions } from '../types';
|
|
7
|
+
interface ViolationState {
|
|
8
|
+
count: number;
|
|
9
|
+
cooldownUntil: number;
|
|
10
|
+
lastViolationAt: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Tracks repeated violations and computes escalating cooldowns.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ViolationTracker {
|
|
16
|
+
private readonly storage;
|
|
17
|
+
/**
|
|
18
|
+
* Create a violation tracker bound to a storage backend.
|
|
19
|
+
*
|
|
20
|
+
* @param storage - Storage backend for violation state.
|
|
21
|
+
*/
|
|
22
|
+
constructor(storage: RateLimitStorage);
|
|
23
|
+
private key;
|
|
24
|
+
/**
|
|
25
|
+
* Read stored violation state for a key, if present.
|
|
26
|
+
*
|
|
27
|
+
* @param key - Storage key for the limiter.
|
|
28
|
+
* @returns Stored violation state or null when none is present.
|
|
29
|
+
*/
|
|
30
|
+
getState(key: string): Promise<ViolationState | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Check if a cooldown is currently active for this key.
|
|
33
|
+
*
|
|
34
|
+
* @param key - Storage key for the limiter.
|
|
35
|
+
* @returns Violation state when cooldown is active, otherwise null.
|
|
36
|
+
*/
|
|
37
|
+
checkCooldown(key: string): Promise<ViolationState | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Record a violation and return the updated state for callers.
|
|
40
|
+
*
|
|
41
|
+
* @param key - Storage key for the limiter.
|
|
42
|
+
* @param baseRetryAfterMs - Base retry delay in milliseconds.
|
|
43
|
+
* @param options - Optional escalation settings.
|
|
44
|
+
* @returns Updated violation state.
|
|
45
|
+
*/
|
|
46
|
+
recordViolation(key: string, baseRetryAfterMs: number, options?: ViolationOptions): Promise<ViolationState>;
|
|
47
|
+
/**
|
|
48
|
+
* Clear stored violation state for a key.
|
|
49
|
+
*
|
|
50
|
+
* @param key - Storage key to reset.
|
|
51
|
+
* @returns Resolves after the violation entry is deleted.
|
|
52
|
+
*/
|
|
53
|
+
reset(key: string): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Violation tracking.
|
|
4
|
+
*
|
|
5
|
+
* Persists repeat violations so cooldowns can escalate predictably.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ViolationTracker = void 0;
|
|
9
|
+
const time_1 = require("../utils/time");
|
|
10
|
+
const DEFAULT_MAX_VIOLATIONS = 5;
|
|
11
|
+
const DEFAULT_ESCALATION_MULTIPLIER = 2;
|
|
12
|
+
const DEFAULT_RESET_AFTER_MS = 60 * 60 * 1000;
|
|
13
|
+
/**
|
|
14
|
+
* Tracks repeated violations and computes escalating cooldowns.
|
|
15
|
+
*/
|
|
16
|
+
class ViolationTracker {
|
|
17
|
+
/**
|
|
18
|
+
* Create a violation tracker bound to a storage backend.
|
|
19
|
+
*
|
|
20
|
+
* @param storage - Storage backend for violation state.
|
|
21
|
+
*/
|
|
22
|
+
constructor(storage) {
|
|
23
|
+
this.storage = storage;
|
|
24
|
+
}
|
|
25
|
+
key(key) {
|
|
26
|
+
return `violation:${key}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Read stored violation state for a key, if present.
|
|
30
|
+
*
|
|
31
|
+
* @param key - Storage key for the limiter.
|
|
32
|
+
* @returns Stored violation state or null when none is present.
|
|
33
|
+
*/
|
|
34
|
+
async getState(key) {
|
|
35
|
+
const stored = await this.storage.get(this.key(key));
|
|
36
|
+
return isViolationState(stored) ? stored : null;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a cooldown is currently active for this key.
|
|
40
|
+
*
|
|
41
|
+
* @param key - Storage key for the limiter.
|
|
42
|
+
* @returns Violation state when cooldown is active, otherwise null.
|
|
43
|
+
*/
|
|
44
|
+
async checkCooldown(key) {
|
|
45
|
+
const state = await this.getState(key);
|
|
46
|
+
if (!state)
|
|
47
|
+
return null;
|
|
48
|
+
if (state.cooldownUntil > Date.now())
|
|
49
|
+
return state;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Record a violation and return the updated state for callers.
|
|
54
|
+
*
|
|
55
|
+
* @param key - Storage key for the limiter.
|
|
56
|
+
* @param baseRetryAfterMs - Base retry delay in milliseconds.
|
|
57
|
+
* @param options - Optional escalation settings.
|
|
58
|
+
* @returns Updated violation state.
|
|
59
|
+
*/
|
|
60
|
+
async recordViolation(key, baseRetryAfterMs, options) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const prev = await this.getState(key);
|
|
63
|
+
const maxViolations = options?.maxViolations ?? DEFAULT_MAX_VIOLATIONS;
|
|
64
|
+
const multiplier = options?.escalationMultiplier ?? DEFAULT_ESCALATION_MULTIPLIER;
|
|
65
|
+
const resetAfter = (0, time_1.resolveDuration)(options?.resetAfter, DEFAULT_RESET_AFTER_MS);
|
|
66
|
+
const count = Math.min((prev?.count ?? 0) + 1, maxViolations);
|
|
67
|
+
const base = Math.max(0, baseRetryAfterMs);
|
|
68
|
+
const cooldownMs = base * Math.pow(multiplier, Math.max(0, count - 1));
|
|
69
|
+
const cooldownUntil = now + cooldownMs;
|
|
70
|
+
const state = {
|
|
71
|
+
count,
|
|
72
|
+
cooldownUntil,
|
|
73
|
+
lastViolationAt: now,
|
|
74
|
+
};
|
|
75
|
+
await this.storage.set(this.key(key), state, resetAfter);
|
|
76
|
+
return state;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Clear stored violation state for a key.
|
|
80
|
+
*
|
|
81
|
+
* @param key - Storage key to reset.
|
|
82
|
+
* @returns Resolves after the violation entry is deleted.
|
|
83
|
+
*/
|
|
84
|
+
async reset(key) {
|
|
85
|
+
await this.storage.delete(this.key(key));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.ViolationTracker = ViolationTracker;
|
|
89
|
+
/**
|
|
90
|
+
* Type guard for violation state entries loaded from storage.
|
|
91
|
+
*
|
|
92
|
+
* @param value - Stored value to validate.
|
|
93
|
+
* @returns True when the value matches the ViolationState shape.
|
|
94
|
+
*/
|
|
95
|
+
function isViolationState(value) {
|
|
96
|
+
if (!value || typeof value !== 'object')
|
|
97
|
+
return false;
|
|
98
|
+
const state = value;
|
|
99
|
+
return (typeof state.count === 'number' &&
|
|
100
|
+
Number.isFinite(state.count) &&
|
|
101
|
+
typeof state.cooldownUntil === 'number' &&
|
|
102
|
+
Number.isFinite(state.cooldownUntil) &&
|
|
103
|
+
typeof state.lastViolationAt === 'number' &&
|
|
104
|
+
Number.isFinite(state.lastViolationAt));
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmlvbGF0aW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9lbmdpbmUvdmlvbGF0aW9ucy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7R0FJRzs7O0FBR0gsd0NBQWdEO0FBUWhELE1BQU0sc0JBQXNCLEdBQUcsQ0FBQyxDQUFDO0FBQ2pDLE1BQU0sNkJBQTZCLEdBQUcsQ0FBQyxDQUFDO0FBQ3hDLE1BQU0sc0JBQXNCLEdBQUcsRUFBRSxHQUFHLEVBQUUsR0FBRyxJQUFJLENBQUM7QUFFOUM7O0dBRUc7QUFDSCxNQUFhLGdCQUFnQjtJQUMzQjs7OztPQUlHO0lBQ0gsWUFBb0MsT0FBeUI7UUFBekIsWUFBTyxHQUFQLE9BQU8sQ0FBa0I7SUFBRyxDQUFDO0lBRXpELEdBQUcsQ0FBQyxHQUFXO1FBQ3JCLE9BQU8sYUFBYSxHQUFHLEVBQUUsQ0FBQztJQUM1QixDQUFDO0lBRUQ7Ozs7O09BS0c7SUFDSCxLQUFLLENBQUMsUUFBUSxDQUFDLEdBQVc7UUFDeEIsTUFBTSxNQUFNLEdBQUcsTUFBTSxJQUFJLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBaUIsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO1FBQ3JFLE9BQU8sZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO0lBQ2xELENBQUM7SUFFRDs7Ozs7T0FLRztJQUNILEtBQUssQ0FBQyxhQUFhLENBQUMsR0FBVztRQUM3QixNQUFNLEtBQUssR0FBRyxNQUFNLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdkMsSUFBSSxDQUFDLEtBQUs7WUFBRSxPQUFPLElBQUksQ0FBQztRQUN4QixJQUFJLEtBQUssQ0FBQyxhQUFhLEdBQUcsSUFBSSxDQUFDLEdBQUcsRUFBRTtZQUFFLE9BQU8sS0FBSyxDQUFDO1FBQ25ELE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUVEOzs7Ozs7O09BT0c7SUFDSCxLQUFLLENBQUMsZUFBZSxDQUNuQixHQUFXLEVBQ1gsZ0JBQXdCLEVBQ3hCLE9BQTBCO1FBRTFCLE1BQU0sR0FBRyxHQUFHLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztRQUN2QixNQUFNLElBQUksR0FBRyxNQUFNLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLENBQUM7UUFDdEMsTUFBTSxhQUFhLEdBQUcsT0FBTyxFQUFFLGFBQWEsSUFBSSxzQkFBc0IsQ0FBQztRQUN2RSxNQUFNLFVBQVUsR0FDZCxPQUFPLEVBQUUsb0JBQW9CLElBQUksNkJBQTZCLENBQUM7UUFDakUsTUFBTSxVQUFVLEdBQUcsSUFBQSxzQkFBZSxFQUNoQyxPQUFPLEVBQUUsVUFBVSxFQUNuQixzQkFBc0IsQ0FDdkIsQ0FBQztRQUVGLE1BQU0sS0FBSyxHQUFHLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxJQUFJLENBQUMsQ0FBQyxHQUFHLENBQUMsRUFBRSxhQUFhLENBQUMsQ0FBQztRQUM5RCxNQUFNLElBQUksR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxnQkFBZ0IsQ0FBQyxDQUFDO1FBQzNDLE1BQU0sVUFBVSxHQUFHLElBQUksR0FBRyxJQUFJLENBQUMsR0FBRyxDQUFDLFVBQVUsRUFBRSxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsRUFBRSxLQUFLLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN2RSxNQUFNLGFBQWEsR0FBRyxHQUFHLEdBQUcsVUFBVSxDQUFDO1FBRXZDLE1BQU0sS0FBSyxHQUFtQjtZQUM1QixLQUFLO1lBQ0wsYUFBYTtZQUNiLGVBQWUsRUFBRSxHQUFHO1NBQ3JCLENBQUM7UUFFRixNQUFNLElBQUksQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUUsS0FBSyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ3pELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUVEOzs7OztPQUtHO0lBQ0gsS0FBSyxDQUFDLEtBQUssQ0FBQyxHQUFXO1FBQ3JCLE1BQU0sSUFBSSxDQUFDLE9BQU8sQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDO0lBQzNDLENBQUM7Q0FDRjtBQW5GRCw0Q0FtRkM7QUFFRDs7Ozs7R0FLRztBQUNILFNBQVMsZ0JBQWdCLENBQUMsS0FBYztJQUN0QyxJQUFJLENBQUMsS0FBSyxJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVE7UUFBRSxPQUFPLEtBQUssQ0FBQztJQUN0RCxNQUFNLEtBQUssR0FBRyxLQUF1QixDQUFDO0lBQ3RDLE9BQU8sQ0FDTCxPQUFPLEtBQUssQ0FBQyxLQUFLLEtBQUssUUFBUTtRQUMvQixNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxLQUFLLENBQUM7UUFDNUIsT0FBTyxLQUFLLENBQUMsYUFBYSxLQUFLLFFBQVE7UUFDdkMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsYUFBYSxDQUFDO1FBQ3BDLE9BQU8sS0FBSyxDQUFDLGVBQWUsS0FBSyxRQUFRO1FBQ3pDLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxDQUN2QyxDQUFDO0FBQ0osQ0FBQyJ9
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate limit error type.
|
|
3
|
+
*
|
|
4
|
+
* Lets callers distinguish rate-limit failures from other errors.
|
|
5
|
+
*/
|
|
6
|
+
import type { RateLimitStoreValue } from './types';
|
|
7
|
+
/**
|
|
8
|
+
* Error thrown by the directive wrapper when a function is rate-limited.
|
|
9
|
+
*
|
|
10
|
+
* @extends Error
|
|
11
|
+
*/
|
|
12
|
+
export declare class RateLimitError extends Error {
|
|
13
|
+
readonly result: RateLimitStoreValue;
|
|
14
|
+
/**
|
|
15
|
+
* Create a rate-limit error with the stored result payload.
|
|
16
|
+
*
|
|
17
|
+
* @param result - Aggregated rate-limit result.
|
|
18
|
+
* @param message - Optional error message override.
|
|
19
|
+
*/
|
|
20
|
+
constructor(result: RateLimitStoreValue, message?: string);
|
|
21
|
+
}
|