@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,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Limiter config resolution.
|
|
4
|
+
*
|
|
5
|
+
* Applies defaults and merges overrides into concrete limiter settings
|
|
6
|
+
* used by the engine and plugin.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DEFAULT_LIMITER = void 0;
|
|
10
|
+
exports.mergeLimiterConfigs = mergeLimiterConfigs;
|
|
11
|
+
exports.resolveLimiterConfig = resolveLimiterConfig;
|
|
12
|
+
exports.resolveLimiterConfigs = resolveLimiterConfigs;
|
|
13
|
+
const time_1 = require("./time");
|
|
14
|
+
const DEFAULT_MAX_REQUESTS = 10;
|
|
15
|
+
const DEFAULT_INTERVAL_MS = 60000;
|
|
16
|
+
const DEFAULT_ALGORITHM = 'fixed-window';
|
|
17
|
+
const DEFAULT_SCOPE = 'user';
|
|
18
|
+
/**
|
|
19
|
+
* Default limiter used when no explicit configuration is provided.
|
|
20
|
+
*/
|
|
21
|
+
exports.DEFAULT_LIMITER = {
|
|
22
|
+
maxRequests: DEFAULT_MAX_REQUESTS,
|
|
23
|
+
interval: DEFAULT_INTERVAL_MS,
|
|
24
|
+
algorithm: DEFAULT_ALGORITHM,
|
|
25
|
+
scope: DEFAULT_SCOPE,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Merge limiter configs; later values override earlier ones for layering.
|
|
29
|
+
*
|
|
30
|
+
* @param configs - Limiter configs ordered from lowest to highest priority.
|
|
31
|
+
* @returns Merged limiter config with later overrides applied.
|
|
32
|
+
*/
|
|
33
|
+
function mergeLimiterConfigs(...configs) {
|
|
34
|
+
return configs.reduce((acc, cfg) => ({ ...acc, ...(cfg ?? {}) }), {});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a limiter config for a single scope with defaults applied.
|
|
38
|
+
*
|
|
39
|
+
* @param config - Base limiter configuration.
|
|
40
|
+
* @param scope - Scope to resolve for the limiter.
|
|
41
|
+
* @returns Resolved limiter config with defaults and derived values.
|
|
42
|
+
*/
|
|
43
|
+
function resolveLimiterConfig(config, scope) {
|
|
44
|
+
const maxRequests = typeof config.maxRequests === 'number' && config.maxRequests > 0
|
|
45
|
+
? config.maxRequests
|
|
46
|
+
: DEFAULT_MAX_REQUESTS;
|
|
47
|
+
const intervalMs = (0, time_1.clampAtLeast)((0, time_1.resolveDuration)(config.interval, DEFAULT_INTERVAL_MS), 1);
|
|
48
|
+
const algorithm = config.algorithm ?? DEFAULT_ALGORITHM;
|
|
49
|
+
const intervalSeconds = intervalMs / 1000;
|
|
50
|
+
const burst = typeof config.burst === 'number' && config.burst > 0
|
|
51
|
+
? config.burst
|
|
52
|
+
: maxRequests;
|
|
53
|
+
const refillRate = typeof config.refillRate === 'number' && config.refillRate > 0
|
|
54
|
+
? config.refillRate
|
|
55
|
+
: maxRequests / intervalSeconds;
|
|
56
|
+
const leakRate = typeof config.leakRate === 'number' && config.leakRate > 0
|
|
57
|
+
? config.leakRate
|
|
58
|
+
: maxRequests / intervalSeconds;
|
|
59
|
+
return {
|
|
60
|
+
maxRequests,
|
|
61
|
+
intervalMs,
|
|
62
|
+
algorithm,
|
|
63
|
+
scope,
|
|
64
|
+
burst,
|
|
65
|
+
refillRate,
|
|
66
|
+
leakRate,
|
|
67
|
+
violations: config.violations,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a stable window id when one is missing.
|
|
72
|
+
*
|
|
73
|
+
* @param window - Window config entry.
|
|
74
|
+
* @param index - Index of the window in the config list.
|
|
75
|
+
* @returns Window id string.
|
|
76
|
+
*/
|
|
77
|
+
function resolveWindowId(window, index) {
|
|
78
|
+
if (window.id && window.id.trim())
|
|
79
|
+
return window.id;
|
|
80
|
+
/**
|
|
81
|
+
* Stable fallback IDs keep window identity deterministic for resets.
|
|
82
|
+
*/
|
|
83
|
+
return `w${index + 1}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Resolve limiter configs for a scope across all configured windows.
|
|
87
|
+
*
|
|
88
|
+
* @param config - Base limiter configuration that may include windows.
|
|
89
|
+
* @param scope - Scope to resolve for the limiter.
|
|
90
|
+
* @returns Resolved limiter configs for each window (or a single config).
|
|
91
|
+
*/
|
|
92
|
+
function resolveLimiterConfigs(config, scope) {
|
|
93
|
+
const windows = config.windows;
|
|
94
|
+
if (!windows || windows.length === 0) {
|
|
95
|
+
return [resolveLimiterConfig(config, scope)];
|
|
96
|
+
}
|
|
97
|
+
const { windows: _windows, ...base } = config;
|
|
98
|
+
return windows.map((window, index) => {
|
|
99
|
+
const windowId = resolveWindowId(window, index);
|
|
100
|
+
const merged = { ...base, ...window };
|
|
101
|
+
const resolved = resolveLimiterConfig(merged, scope);
|
|
102
|
+
return windowId ? { ...resolved, windowId } : resolved;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlnLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL2NvbmZpZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7O0dBS0c7OztBQWdDSCxrREFPQztBQVNELG9EQXlDQztBQXdCRCxzREFpQkM7QUF6SEQsaUNBQXVEO0FBRXZELE1BQU0sb0JBQW9CLEdBQUcsRUFBRSxDQUFDO0FBQ2hDLE1BQU0sbUJBQW1CLEdBQUcsS0FBTSxDQUFDO0FBQ25DLE1BQU0saUJBQWlCLEdBQTJCLGNBQWMsQ0FBQztBQUNqRSxNQUFNLGFBQWEsR0FBbUIsTUFBTSxDQUFDO0FBRTdDOztHQUVHO0FBQ1UsUUFBQSxlQUFlLEdBQTJCO0lBQ3JELFdBQVcsRUFBRSxvQkFBb0I7SUFDakMsUUFBUSxFQUFFLG1CQUFtQjtJQUM3QixTQUFTLEVBQUUsaUJBQWlCO0lBQzVCLEtBQUssRUFBRSxhQUFhO0NBQ3JCLENBQUM7QUFFRjs7Ozs7R0FLRztBQUNILFNBQWdCLG1CQUFtQixDQUNqQyxHQUFHLE9BQWtEO0lBRXJELE9BQU8sT0FBTyxDQUFDLE1BQU0sQ0FDbkIsQ0FBQyxHQUFHLEVBQUUsR0FBRyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEVBQUUsR0FBRyxHQUFHLEVBQUUsR0FBRyxDQUFDLEdBQUcsSUFBSSxFQUFFLENBQUMsRUFBRSxDQUFDLEVBQzFDLEVBQUUsQ0FDSCxDQUFDO0FBQ0osQ0FBQztBQUVEOzs7Ozs7R0FNRztBQUNILFNBQWdCLG9CQUFvQixDQUNsQyxNQUE4QixFQUM5QixLQUFxQjtJQUVyQixNQUFNLFdBQVcsR0FDZixPQUFPLE1BQU0sQ0FBQyxXQUFXLEtBQUssUUFBUSxJQUFJLE1BQU0sQ0FBQyxXQUFXLEdBQUcsQ0FBQztRQUM5RCxDQUFDLENBQUMsTUFBTSxDQUFDLFdBQVc7UUFDcEIsQ0FBQyxDQUFDLG9CQUFvQixDQUFDO0lBRTNCLE1BQU0sVUFBVSxHQUFHLElBQUEsbUJBQVksRUFDN0IsSUFBQSxzQkFB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
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key construction helpers.
|
|
3
|
+
*
|
|
4
|
+
* Builds consistent storage keys for scopes and exemptions across
|
|
5
|
+
* message and interaction sources so limits remain comparable.
|
|
6
|
+
*/
|
|
7
|
+
import { Message } from 'discord.js';
|
|
8
|
+
import type { Interaction } from 'discord.js';
|
|
9
|
+
import type { Context } from 'commandkit';
|
|
10
|
+
import type { LoadedCommand } from 'commandkit';
|
|
11
|
+
import type { RateLimitExemptionScope, RateLimitKeyResolver, RateLimitScope } from '../types';
|
|
12
|
+
/**
|
|
13
|
+
* Inputs for resolving a scope-based key from a command/source.
|
|
14
|
+
*/
|
|
15
|
+
export interface ResolveScopeKeyParams {
|
|
16
|
+
ctx: Context;
|
|
17
|
+
source: Interaction | Message;
|
|
18
|
+
command: LoadedCommand;
|
|
19
|
+
scope: RateLimitScope;
|
|
20
|
+
keyPrefix?: string;
|
|
21
|
+
keyResolver?: RateLimitKeyResolver;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolved key paired with its scope for aggregation.
|
|
25
|
+
*/
|
|
26
|
+
export interface ResolvedScopeKey {
|
|
27
|
+
scope: RateLimitScope;
|
|
28
|
+
key: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract role IDs from a message/interaction for role-based limits.
|
|
32
|
+
*
|
|
33
|
+
* @param source - Interaction or message to read role data from.
|
|
34
|
+
* @returns Array of role IDs for the source, or an empty array.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getRoleIds(source: Interaction | Message): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Build a storage key for a temporary exemption entry.
|
|
39
|
+
*
|
|
40
|
+
* @param scope - Exemption scope to encode.
|
|
41
|
+
* @param id - Scope identifier (user, guild, role, etc.).
|
|
42
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
43
|
+
* @returns Fully-qualified exemption storage key.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildExemptionKey(scope: RateLimitExemptionScope, id: string, keyPrefix?: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Build a prefix for scanning exemption keys in storage.
|
|
48
|
+
*
|
|
49
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
50
|
+
* @param scope - Optional exemption scope to narrow the prefix.
|
|
51
|
+
* @returns Prefix suitable for storage scans.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildExemptionPrefix(keyPrefix?: string, scope?: RateLimitExemptionScope): string;
|
|
54
|
+
/**
|
|
55
|
+
* Parse an exemption key into scope and ID for listing.
|
|
56
|
+
*
|
|
57
|
+
* @param key - Exemption key to parse.
|
|
58
|
+
* @param keyPrefix - Optional prefix to strip before parsing.
|
|
59
|
+
* @returns Parsed scope/id pair or null when the key is invalid.
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseExemptionKey(key: string, keyPrefix?: string): {
|
|
62
|
+
scope: RateLimitExemptionScope;
|
|
63
|
+
id: string;
|
|
64
|
+
} | null;
|
|
65
|
+
/**
|
|
66
|
+
* Resolve all exemption keys that could apply to a source.
|
|
67
|
+
*
|
|
68
|
+
* @param source - Interaction or message to resolve keys for.
|
|
69
|
+
* @param keyPrefix - Optional prefix to prepend to keys.
|
|
70
|
+
* @returns Exemption keys that should be checked for the source.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveExemptionKeys(source: Interaction | Message, keyPrefix?: string): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the storage key for a single scope.
|
|
75
|
+
*
|
|
76
|
+
* @param params - Inputs required to resolve the scope key.
|
|
77
|
+
* @returns Resolved scope key or null when required identifiers are missing.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveScopeKey({ ctx, source, command, scope, keyPrefix, keyResolver, }: ResolveScopeKeyParams): ResolvedScopeKey | null;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve keys for multiple scopes, dropping unresolvable ones.
|
|
82
|
+
*
|
|
83
|
+
* @param params - Inputs required to resolve all scope keys.
|
|
84
|
+
* @returns Array of resolved scope keys.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveScopeKeys(params: Omit<ResolveScopeKeyParams, 'scope'> & {
|
|
87
|
+
scopes: RateLimitScope[];
|
|
88
|
+
}): ResolvedScopeKey[];
|
|
89
|
+
/**
|
|
90
|
+
* Build a prefix for resets by scope/identifier.
|
|
91
|
+
*
|
|
92
|
+
* @param scope - Scope to build the prefix for.
|
|
93
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
94
|
+
* @param identifiers - Identifiers required for the scope.
|
|
95
|
+
* @returns Prefix string or null when identifiers are missing.
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildScopePrefix(scope: RateLimitScope, keyPrefix: string | undefined, identifiers: {
|
|
98
|
+
userId?: string;
|
|
99
|
+
guildId?: string;
|
|
100
|
+
channelId?: string;
|
|
101
|
+
commandName?: string;
|
|
102
|
+
}): string | null;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Key construction helpers.
|
|
4
|
+
*
|
|
5
|
+
* Builds consistent storage keys for scopes and exemptions across
|
|
6
|
+
* message and interaction sources so limits remain comparable.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getRoleIds = getRoleIds;
|
|
10
|
+
exports.buildExemptionKey = buildExemptionKey;
|
|
11
|
+
exports.buildExemptionPrefix = buildExemptionPrefix;
|
|
12
|
+
exports.parseExemptionKey = parseExemptionKey;
|
|
13
|
+
exports.resolveExemptionKeys = resolveExemptionKeys;
|
|
14
|
+
exports.resolveScopeKey = resolveScopeKey;
|
|
15
|
+
exports.resolveScopeKeys = resolveScopeKeys;
|
|
16
|
+
exports.buildScopePrefix = buildScopePrefix;
|
|
17
|
+
const discord_js_1 = require("discord.js");
|
|
18
|
+
const types_1 = require("../types");
|
|
19
|
+
const constants_1 = require("../constants");
|
|
20
|
+
/**
|
|
21
|
+
* Apply an optional prefix to a storage key.
|
|
22
|
+
*
|
|
23
|
+
* @param prefix - Optional prefix to prepend.
|
|
24
|
+
* @param key - Base key to prefix.
|
|
25
|
+
* @returns Prefixed key.
|
|
26
|
+
*/
|
|
27
|
+
function applyPrefix(prefix, key) {
|
|
28
|
+
if (!prefix)
|
|
29
|
+
return key;
|
|
30
|
+
return `${prefix}${key}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a user id from a message or interaction.
|
|
34
|
+
*
|
|
35
|
+
* @param source - Interaction or message source.
|
|
36
|
+
* @returns User id or null when unavailable.
|
|
37
|
+
*/
|
|
38
|
+
function getUserId(source) {
|
|
39
|
+
if (source instanceof discord_js_1.Message)
|
|
40
|
+
return source.author.id;
|
|
41
|
+
return source.user?.id ?? null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve a guild id from a message or interaction.
|
|
45
|
+
*
|
|
46
|
+
* @param source - Interaction or message source.
|
|
47
|
+
* @returns Guild id or null when unavailable.
|
|
48
|
+
*/
|
|
49
|
+
function getGuildId(source) {
|
|
50
|
+
if (source instanceof discord_js_1.Message)
|
|
51
|
+
return source.guildId ?? null;
|
|
52
|
+
return source.guildId ?? null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a channel id from a message or interaction.
|
|
56
|
+
*
|
|
57
|
+
* @param source - Interaction or message source.
|
|
58
|
+
* @returns Channel id or null when unavailable.
|
|
59
|
+
*/
|
|
60
|
+
function getChannelId(source) {
|
|
61
|
+
if (source instanceof discord_js_1.Message)
|
|
62
|
+
return source.channelId ?? null;
|
|
63
|
+
return source.channelId ?? null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a parent category id from a channel object.
|
|
67
|
+
*
|
|
68
|
+
* @param channel - Channel object to inspect.
|
|
69
|
+
* @returns Parent id or null when unavailable.
|
|
70
|
+
*/
|
|
71
|
+
function getParentId(channel) {
|
|
72
|
+
if (!channel || typeof channel !== 'object')
|
|
73
|
+
return null;
|
|
74
|
+
if (!('parentId' in channel))
|
|
75
|
+
return null;
|
|
76
|
+
const parentId = channel.parentId;
|
|
77
|
+
return parentId ?? null;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a category id from a message or interaction.
|
|
81
|
+
*
|
|
82
|
+
* @param source - Interaction or message source.
|
|
83
|
+
* @returns Category id or null when unavailable.
|
|
84
|
+
*/
|
|
85
|
+
function getCategoryId(source) {
|
|
86
|
+
if (source instanceof discord_js_1.Message) {
|
|
87
|
+
return getParentId(source.channel);
|
|
88
|
+
}
|
|
89
|
+
return getParentId(source.channel);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Extract role IDs from a message/interaction for role-based limits.
|
|
93
|
+
*
|
|
94
|
+
* @param source - Interaction or message to read role data from.
|
|
95
|
+
* @returns Array of role IDs for the source, or an empty array.
|
|
96
|
+
*/
|
|
97
|
+
function getRoleIds(source) {
|
|
98
|
+
const roles = source.member?.roles;
|
|
99
|
+
if (!roles)
|
|
100
|
+
return [];
|
|
101
|
+
if (Array.isArray(roles))
|
|
102
|
+
return roles;
|
|
103
|
+
if ('cache' in roles) {
|
|
104
|
+
return roles.cache.map((role) => role.id);
|
|
105
|
+
}
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Build a storage key for a temporary exemption entry.
|
|
110
|
+
*
|
|
111
|
+
* @param scope - Exemption scope to encode.
|
|
112
|
+
* @param id - Scope identifier (user, guild, role, etc.).
|
|
113
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
114
|
+
* @returns Fully-qualified exemption storage key.
|
|
115
|
+
*/
|
|
116
|
+
function buildExemptionKey(scope, id, keyPrefix) {
|
|
117
|
+
const prefix = keyPrefix ?? '';
|
|
118
|
+
return applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}exempt:${scope}:${id}`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Build a prefix for scanning exemption keys in storage.
|
|
122
|
+
*
|
|
123
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
124
|
+
* @param scope - Optional exemption scope to narrow the prefix.
|
|
125
|
+
* @returns Prefix suitable for storage scans.
|
|
126
|
+
*/
|
|
127
|
+
function buildExemptionPrefix(keyPrefix, scope) {
|
|
128
|
+
const prefix = keyPrefix ?? '';
|
|
129
|
+
const base = `${constants_1.DEFAULT_KEY_PREFIX}exempt:`;
|
|
130
|
+
if (!scope)
|
|
131
|
+
return applyPrefix(prefix, base);
|
|
132
|
+
return applyPrefix(prefix, `${base}${scope}:`);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse an exemption key into scope and ID for listing.
|
|
136
|
+
*
|
|
137
|
+
* @param key - Exemption key to parse.
|
|
138
|
+
* @param keyPrefix - Optional prefix to strip before parsing.
|
|
139
|
+
* @returns Parsed scope/id pair or null when the key is invalid.
|
|
140
|
+
*/
|
|
141
|
+
function parseExemptionKey(key, keyPrefix) {
|
|
142
|
+
const prefix = keyPrefix ?? '';
|
|
143
|
+
const base = `${prefix}${constants_1.DEFAULT_KEY_PREFIX}exempt:`;
|
|
144
|
+
if (!key.startsWith(base))
|
|
145
|
+
return null;
|
|
146
|
+
const rest = key.slice(base.length);
|
|
147
|
+
const [scope, ...idParts] = rest.split(':');
|
|
148
|
+
if (!scope || idParts.length === 0)
|
|
149
|
+
return null;
|
|
150
|
+
if (!types_1.RATE_LIMIT_EXEMPTION_SCOPES.includes(scope)) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
return { scope: scope, id: idParts.join(':') };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve all exemption keys that could apply to a source.
|
|
157
|
+
*
|
|
158
|
+
* @param source - Interaction or message to resolve keys for.
|
|
159
|
+
* @param keyPrefix - Optional prefix to prepend to keys.
|
|
160
|
+
* @returns Exemption keys that should be checked for the source.
|
|
161
|
+
*/
|
|
162
|
+
function resolveExemptionKeys(source, keyPrefix) {
|
|
163
|
+
const keys = [];
|
|
164
|
+
const userId = getUserId(source);
|
|
165
|
+
if (userId) {
|
|
166
|
+
keys.push(buildExemptionKey('user', userId, keyPrefix));
|
|
167
|
+
}
|
|
168
|
+
const guildId = getGuildId(source);
|
|
169
|
+
if (guildId) {
|
|
170
|
+
keys.push(buildExemptionKey('guild', guildId, keyPrefix));
|
|
171
|
+
}
|
|
172
|
+
const channelId = getChannelId(source);
|
|
173
|
+
if (channelId) {
|
|
174
|
+
keys.push(buildExemptionKey('channel', channelId, keyPrefix));
|
|
175
|
+
}
|
|
176
|
+
const categoryId = getCategoryId(source);
|
|
177
|
+
if (categoryId) {
|
|
178
|
+
keys.push(buildExemptionKey('category', categoryId, keyPrefix));
|
|
179
|
+
}
|
|
180
|
+
const roleIds = getRoleIds(source);
|
|
181
|
+
for (const roleId of roleIds) {
|
|
182
|
+
keys.push(buildExemptionKey('role', roleId, keyPrefix));
|
|
183
|
+
}
|
|
184
|
+
return keys;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the storage key for a single scope.
|
|
188
|
+
*
|
|
189
|
+
* @param params - Inputs required to resolve the scope key.
|
|
190
|
+
* @returns Resolved scope key or null when required identifiers are missing.
|
|
191
|
+
*/
|
|
192
|
+
function resolveScopeKey({ ctx, source, command, scope, keyPrefix, keyResolver, }) {
|
|
193
|
+
const prefix = keyPrefix ?? '';
|
|
194
|
+
const commandName = ctx.commandName || command.command.name;
|
|
195
|
+
switch (scope) {
|
|
196
|
+
case 'user': {
|
|
197
|
+
const userId = getUserId(source);
|
|
198
|
+
if (!userId)
|
|
199
|
+
return null;
|
|
200
|
+
return {
|
|
201
|
+
scope,
|
|
202
|
+
key: applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}user:${userId}:${commandName}`),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
case 'guild': {
|
|
206
|
+
const guildId = getGuildId(source);
|
|
207
|
+
if (!guildId)
|
|
208
|
+
return null;
|
|
209
|
+
return {
|
|
210
|
+
scope,
|
|
211
|
+
key: applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}guild:${guildId}:${commandName}`),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case 'channel': {
|
|
215
|
+
const channelId = getChannelId(source);
|
|
216
|
+
if (!channelId)
|
|
217
|
+
return null;
|
|
218
|
+
return {
|
|
219
|
+
scope,
|
|
220
|
+
key: applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}channel:${channelId}:${commandName}`),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
case 'global': {
|
|
224
|
+
return {
|
|
225
|
+
scope,
|
|
226
|
+
key: applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}global:${commandName}`),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
case 'user-guild': {
|
|
230
|
+
const userId = getUserId(source);
|
|
231
|
+
const guildId = getGuildId(source);
|
|
232
|
+
if (!userId || !guildId)
|
|
233
|
+
return null;
|
|
234
|
+
return {
|
|
235
|
+
scope,
|
|
236
|
+
key: applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}user:${userId}:guild:${guildId}:${commandName}`),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
case 'custom': {
|
|
240
|
+
if (!keyResolver)
|
|
241
|
+
return null;
|
|
242
|
+
const customKey = keyResolver(ctx, command, source);
|
|
243
|
+
if (!customKey)
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
scope,
|
|
247
|
+
key: applyPrefix(prefix, customKey),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
default:
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Resolve keys for multiple scopes, dropping unresolvable ones.
|
|
256
|
+
*
|
|
257
|
+
* @param params - Inputs required to resolve all scope keys.
|
|
258
|
+
* @returns Array of resolved scope keys.
|
|
259
|
+
*/
|
|
260
|
+
function resolveScopeKeys(params) {
|
|
261
|
+
const results = [];
|
|
262
|
+
for (const scope of params.scopes) {
|
|
263
|
+
const resolved = resolveScopeKey({ ...params, scope });
|
|
264
|
+
if (resolved)
|
|
265
|
+
results.push(resolved);
|
|
266
|
+
}
|
|
267
|
+
return results;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build a prefix for resets by scope/identifier.
|
|
271
|
+
*
|
|
272
|
+
* @param scope - Scope to build the prefix for.
|
|
273
|
+
* @param keyPrefix - Optional prefix to prepend to the key.
|
|
274
|
+
* @param identifiers - Identifiers required for the scope.
|
|
275
|
+
* @returns Prefix string or null when identifiers are missing.
|
|
276
|
+
*/
|
|
277
|
+
function buildScopePrefix(scope, keyPrefix, identifiers) {
|
|
278
|
+
const prefix = keyPrefix ?? '';
|
|
279
|
+
switch (scope) {
|
|
280
|
+
case 'user':
|
|
281
|
+
return identifiers.userId
|
|
282
|
+
? applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}user:${identifiers.userId}:`)
|
|
283
|
+
: null;
|
|
284
|
+
case 'guild':
|
|
285
|
+
return identifiers.guildId
|
|
286
|
+
? applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}guild:${identifiers.guildId}:`)
|
|
287
|
+
: null;
|
|
288
|
+
case 'channel':
|
|
289
|
+
return identifiers.channelId
|
|
290
|
+
? applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}channel:${identifiers.channelId}:`)
|
|
291
|
+
: null;
|
|
292
|
+
case 'global':
|
|
293
|
+
return applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}global:`);
|
|
294
|
+
case 'user-guild':
|
|
295
|
+
return identifiers.userId && identifiers.guildId
|
|
296
|
+
? applyPrefix(prefix, `${constants_1.DEFAULT_KEY_PREFIX}user:${identifiers.userId}:guild:${identifiers.guildId}:`)
|
|
297
|
+
: null;
|
|
298
|
+
case 'custom':
|
|
299
|
+
return null;
|
|
300
|
+
default:
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2V5cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9rZXlzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7Ozs7R0FLRzs7QUErR0gsZ0NBUUM7QUFVRCw4Q0FPQztBQVNELG9EQVFDO0FBU0QsOENBY0M7QUFTRCxvREFnQ0M7QUFRRCwwQ0EyRUM7QUFRRCw0Q0FXQztBQVVELDRDQStDQztBQXRYRCwyQ0FBcUM7QUFTckMsb0NBQXVEO0FBQ3ZELDRDQUFrRDtBQXNCbEQ7Ozs7OztHQU1HO0FBQ0gsU0FBUyxXQUFXLENBQUMsTUFBMEIsRUFBRSxHQUFXO0lBQzFELElBQUksQ0FBQyxNQUFNO1FBQUUsT0FBTyxHQUFHLENBQUM7SUFDeEIsT0FBTyxHQUFHLE1BQU0sR0FBRyxHQUFHLEVBQUUsQ0FBQztBQUMzQixDQUFDO0FBRUQ7Ozs7O0dBS0c7QUFDSCxTQUFTLFNBQVMsQ0FBQyxNQUE2QjtJQUM5QyxJQUFJLE1BQU0sWUFBWSxvQkFBTztRQUFFLE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7SUFDdkQsT0FBTyxNQUFNLENBQUMsSUFBSSxFQUFFLEVBQUUsSUFBSSxJQUFJLENBQUM7QUFDakMsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsU0FBUyxVQUFVLENBQUMsTUFBNkI7SUFDL0MsSUFBSSxNQUFNLFlBQVksb0JBQU87UUFBRSxPQUFPLE1BQU0sQ0FBQyxPQUFPLElBQUksSUFBSSxDQUFDO0lBQzdELE9BQU8sTUFBTSxDQUFDLE9BQU8sSUFBSSxJQUFJLENBQUM7QUFDaEMsQ0FBQztBQUVEOzs7OztHQUtHO0FBQ0gsU0FBUyxZQUFZLENBQUMsTUFBNkI7SUFDakQsSUFBSSxNQUFNLFlBQVksb0JBQU87UUFBRSxPQUFPLE1BQU0sQ0FBQyxTQUFTLElBQUksSUFBSSxDQUFDO0lBQy9ELE9BQU8sTUFBTSxDQUFDLFNBQVMsSUFBSSxJQUFJLENBQUM7QUFDbEMsQ0FBQztBQUVEOzs7OztHQUtH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
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage-scoped locking helpers.
|
|
3
|
+
*
|
|
4
|
+
* Serializes fallback storage operations per key to reduce same-process races.
|
|
5
|
+
*/
|
|
6
|
+
import type { RateLimitStorage } from '../types';
|
|
7
|
+
type LockedFn<T> = () => Promise<T>;
|
|
8
|
+
/**
|
|
9
|
+
* Serialize work for a storage key to avoid same-process conflicts.
|
|
10
|
+
*
|
|
11
|
+
* @param storage - Storage instance that owns the key.
|
|
12
|
+
* @param key - Storage key to lock on.
|
|
13
|
+
* @param fn - Async function to run under the lock.
|
|
14
|
+
* @returns Result of the locked function.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withStorageKeyLock<T>(storage: RateLimitStorage, key: string, fn: LockedFn<T>): Promise<T>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Storage-scoped locking helpers.
|
|
4
|
+
*
|
|
5
|
+
* Serializes fallback storage operations per key to reduce same-process races.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.withStorageKeyLock = withStorageKeyLock;
|
|
9
|
+
/**
|
|
10
|
+
* Queue-based mutex keyed by string identifiers.
|
|
11
|
+
*/
|
|
12
|
+
class KeyedMutex {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.queues = new Map();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run a function exclusively for the given key.
|
|
18
|
+
*
|
|
19
|
+
* @param key - Key to serialize on.
|
|
20
|
+
* @param fn - Async function to run under the lock.
|
|
21
|
+
* @returns Result of the locked function.
|
|
22
|
+
*/
|
|
23
|
+
async run(key, fn) {
|
|
24
|
+
const previous = this.queues.get(key) ?? Promise.resolve();
|
|
25
|
+
let release;
|
|
26
|
+
const current = new Promise((resolve) => {
|
|
27
|
+
release = resolve;
|
|
28
|
+
});
|
|
29
|
+
const tail = previous.then(() => current);
|
|
30
|
+
this.queues.set(key, tail);
|
|
31
|
+
await previous;
|
|
32
|
+
try {
|
|
33
|
+
return await fn();
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
release();
|
|
37
|
+
if (this.queues.get(key) === tail) {
|
|
38
|
+
this.queues.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const mutexByStorage = new WeakMap();
|
|
44
|
+
/**
|
|
45
|
+
* Serialize work for a storage key to avoid same-process conflicts.
|
|
46
|
+
*
|
|
47
|
+
* @param storage - Storage instance that owns the key.
|
|
48
|
+
* @param key - Storage key to lock on.
|
|
49
|
+
* @param fn - Async function to run under the lock.
|
|
50
|
+
* @returns Result of the locked function.
|
|
51
|
+
*/
|
|
52
|
+
async function withStorageKeyLock(storage, key, fn) {
|
|
53
|
+
let mutex = mutexByStorage.get(storage);
|
|
54
|
+
if (!mutex) {
|
|
55
|
+
mutex = new KeyedMutex();
|
|
56
|
+
mutexByStorage.set(storage, mutex);
|
|
57
|
+
}
|
|
58
|
+
return mutex.run(key, fn);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibG9ja2luZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy91dGlscy9sb2NraW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7QUFBQTs7OztHQUlHOztBQWtESCxnREFXQztBQXZERDs7R0FFRztBQUNILE1BQU0sVUFBVTtJQUFoQjtRQUNtQixXQUFNLEdBQUcsSUFBSSxHQUFHLEVBQXlCLENBQUM7SUE0QjdELENBQUM7SUExQkM7Ozs7OztPQU1HO0lBQ0ksS0FBSyxDQUFDLEdBQUcsQ0FBSSxHQUFXLEVBQUUsRUFBZTtRQUM5QyxNQUFNLFFBQVEsR0FBRyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsSUFBSSxPQUFPLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDM0QsSUFBSSxPQUFtQixDQUFDO1FBQ3hCLE1BQU0sT0FBTyxHQUFHLElBQUksT0FBTyxDQUFPLENBQUMsT0FBTyxFQUFFLEVBQUU7WUFDNUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztRQUNwQixDQUFDLENBQUMsQ0FBQztRQUNILE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDMUMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxDQUFDO1FBRTNCLE1BQU0sUUFBUSxDQUFDO1FBQ2YsSUFBSSxDQUFDO1lBQ0gsT0FBTyxNQUFNLEVBQUUsRUFBRSxDQUFDO1FBQ3BCLENBQUM7Z0JBQVMsQ0FBQztZQUNULE9BQVEsRUFBRSxDQUFDO1lBQ1gsSUFBSSxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsS0FBSyxJQUFJLEVBQUUsQ0FBQztnQkFDbEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDMUIsQ0FBQztRQUNILENBQUM7SUFDSCxDQUFDO0NBQ0Y7QUFFRCxNQUFNLGNBQWMsR0FBRyxJQUFJLE9BQU8sRUFBZ0MsQ0FBQztBQUVuRTs7Ozs7OztHQU9HO0FBQ0ksS0FBSyxVQUFVLGtCQUFrQixDQUN0QyxPQUF5QixFQUN6QixHQUFXLEVBQ1gsRUFBZTtJQUVmLElBQUksS0FBSyxHQUFHLGNBQWMsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDeEMsSUFBSSxDQUFDLEtBQUssRUFBRSxDQUFDO1FBQ1gsS0FBSyxHQUFHLElBQUksVUFBVSxFQUFFLENBQUM7UUFDekIsY0FBYyxDQUFDLEdBQUcsQ0FBQyxPQUFPLEVBQUUsS0FBSyxDQUFDLENBQUM7SUFDckMsQ0FBQztJQUNELE9BQU8sS0FBSyxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFDNUIsQ0FBQyJ9
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time helpers for rate limits.
|
|
3
|
+
*
|
|
4
|
+
* Converts user-friendly durations into milliseconds and clamps values
|
|
5
|
+
* so storage and algorithms always receive safe inputs.
|
|
6
|
+
*/
|
|
7
|
+
import type { DurationLike } from '../types';
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a duration input into milliseconds with a fallback.
|
|
10
|
+
*
|
|
11
|
+
* @param value - Duration input as ms or string.
|
|
12
|
+
* @param fallback - Fallback value used when parsing fails.
|
|
13
|
+
* @returns Parsed duration in milliseconds.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveDuration(value: DurationLike | undefined, fallback: number): number;
|
|
16
|
+
/**
|
|
17
|
+
* Clamp a number to a minimum value to avoid zero/negative windows.
|
|
18
|
+
*
|
|
19
|
+
* @param value - Value to clamp.
|
|
20
|
+
* @param min - Minimum allowed value.
|
|
21
|
+
* @returns The clamped value.
|
|
22
|
+
*/
|
|
23
|
+
export declare function clampAtLeast(value: number, min: number): number;
|