@limitkit/core 0.1.0

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/dist/index.mjs ADDED
@@ -0,0 +1,347 @@
1
+ // src/exceptions/bad-arguments-exception.ts
2
+ var BadArgumentsException = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "BAD_ARGUMENTS_EXCEPTION";
6
+ }
7
+ };
8
+
9
+ // src/exceptions/empty-rules-exception.ts
10
+ var EmptyRulesException = class extends Error {
11
+ constructor() {
12
+ super("The rate limit rules are empty. Ensure there is at least one rule");
13
+ this.name = "EMPTY_RULES_EXCEPTION";
14
+ }
15
+ };
16
+
17
+ // src/exceptions/unknown-algorithm-exception.ts
18
+ var UnknownAlgorithmException = class extends Error {
19
+ constructor(algorithm) {
20
+ super(`Found unknown algorithm: ${algorithm}`);
21
+ this.name = "UNKNOWN_ALGORITHM_EXCEPTION";
22
+ }
23
+ };
24
+
25
+ // src/utils/add-config-to-key.ts
26
+ import { createHash } from "crypto";
27
+ function addConfigToKey(config, key) {
28
+ const sortedKeys = Object.keys(config).sort();
29
+ const sortedConfig = sortedKeys.reduce((acc, k) => {
30
+ acc[k] = config[k];
31
+ return acc;
32
+ }, {});
33
+ const configJson = JSON.stringify(sortedConfig);
34
+ const hashedConfig = createHash("sha256").update(configJson).digest("hex");
35
+ const modifiedKey = `ratelimit:${config.name}:${hashedConfig}:${key}`;
36
+ return modifiedKey;
37
+ }
38
+
39
+ // src/utils/merge-rules.ts
40
+ function mergeRules(globalRules = [], localRules = []) {
41
+ const map = /* @__PURE__ */ new Map();
42
+ for (const rule of globalRules) {
43
+ map.set(rule.name, rule);
44
+ }
45
+ for (const rule of localRules) {
46
+ if (map.has(rule.name)) {
47
+ map.set(rule.name, {
48
+ ...map.get(rule.name),
49
+ ...rule
50
+ });
51
+ } else {
52
+ map.set(rule.name, rule);
53
+ }
54
+ }
55
+ return [...map.values()];
56
+ }
57
+
58
+ // src/rate-limiter.ts
59
+ var RateLimiter = class {
60
+ rules = [];
61
+ debug = false;
62
+ store;
63
+ /**
64
+ * Create a new rate limiter instance.
65
+ * @throws {EmptyRulesException} If the list of rules is empty
66
+ * @param config - Configuration for the rate limiter
67
+ * @see RateLimitConfig
68
+ */
69
+ constructor({ rules, debug, store }) {
70
+ if (rules.length === 0) throw new EmptyRulesException();
71
+ this.rules = rules ?? this.rules;
72
+ this.debug = debug ?? this.debug;
73
+ this.store = store;
74
+ }
75
+ /**
76
+ * Return the configuration object
77
+ * @returns {RateLimitConfig<C>}
78
+ */
79
+ get config() {
80
+ return { rules: this.rules, debug: this.debug, store: this.store };
81
+ }
82
+ /**
83
+ * Check if a request should be allowed under the configured rate limits.
84
+ *
85
+ * Evaluates each rule in order from left to right. Returns as soon as a rule is exceeded (request denied).
86
+ * If all rules allow the request, returns the result of the last rule evaluated.
87
+ *
88
+ * Each rule resolution (key, cost, policy) can be static or dynamic:
89
+ * - Static: evaluated once and reused
90
+ * - Dynamic: evaluated per request based on context
91
+ * - Async: evaluated asynchronously (e.g., database lookups)
92
+ *
93
+ * @param ctx - Request context passed to rule resolvers to determine dynamic values.
94
+ * @returns Promise resolving to the rate limit result. If debug mode is enabled,
95
+ * includes details about each evaluated rule and which rule failed (if any).
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const result = await limiter.consume({
100
+ * userId: 'user-123',
101
+ * ip: '192.168.1.1',
102
+ * endpoint: '/api/search'
103
+ * });
104
+ *
105
+ * if (!result.allowed) {
106
+ * console.log(`Rate limited. Retry in ${result.retryAfter} seconds`);
107
+ * }
108
+ * ```
109
+ */
110
+ async consume(ctx) {
111
+ let result;
112
+ let minRemaining = Infinity;
113
+ let maxReset = 0;
114
+ let minLimit = Infinity;
115
+ const debugRules = [];
116
+ for (const rule of this.rules) {
117
+ const algorithm = typeof rule.policy === "function" ? await rule.policy(ctx) : rule.policy;
118
+ const key = typeof rule.key === "function" ? await rule.key(ctx) : rule.key;
119
+ const cost = typeof rule.cost === "function" ? await rule.cost(ctx) : rule.cost;
120
+ if (cost && cost <= 0)
121
+ throw new BadArgumentsException(
122
+ `Cost must be a positive integer, got cost=${cost}`
123
+ );
124
+ const keyWithConfig = addConfigToKey(algorithm.config, key);
125
+ result = await this.store.consume(
126
+ keyWithConfig,
127
+ algorithm,
128
+ Date.now(),
129
+ cost ?? 1
130
+ );
131
+ minRemaining = Math.min(result.remaining, minRemaining);
132
+ minLimit = Math.min(result.limit, minLimit);
133
+ maxReset = Math.max(result.reset, maxReset);
134
+ if (this.debug) {
135
+ debugRules.push({ ...result, name: rule.name });
136
+ if (result.allowed) console.log(debugRules);
137
+ else console.error(debugRules);
138
+ }
139
+ if (result.remaining === 0) {
140
+ if (this.debug) {
141
+ const debugResults = {
142
+ failedRule: rule.name,
143
+ ...result,
144
+ details: debugRules
145
+ };
146
+ return debugResults;
147
+ }
148
+ return {
149
+ ...result,
150
+ reset: maxReset,
151
+ limit: minLimit,
152
+ remaining: minRemaining
153
+ };
154
+ }
155
+ }
156
+ if (this.debug) {
157
+ const final = {
158
+ ...result,
159
+ details: this.debug ? debugRules : void 0
160
+ };
161
+ console.log(final);
162
+ return final;
163
+ }
164
+ return {
165
+ ...result,
166
+ reset: maxReset,
167
+ limit: minLimit,
168
+ remaining: minRemaining
169
+ };
170
+ }
171
+ };
172
+
173
+ // src/algorithms/fixed-window.ts
174
+ var FixedWindow = class {
175
+ constructor(config) {
176
+ this.config = config;
177
+ }
178
+ /**
179
+ * Validates the fixed window configuration.
180
+ *
181
+ * Ensures the configured window size and request limit are positive values.
182
+ *
183
+ * @throws BadArgumentsException
184
+ * Thrown if:
185
+ * - `limit <= 0`
186
+ * - `window <= 0`
187
+ */
188
+ validate() {
189
+ if (this.config.limit <= 0)
190
+ throw new BadArgumentsException(
191
+ `Expected limit to be positive, got limit=${this.config.limit}`
192
+ );
193
+ if (this.config.window <= 0)
194
+ throw new BadArgumentsException(
195
+ `Expected window to be positive, got window=${this.config.window}`
196
+ );
197
+ }
198
+ };
199
+
200
+ // src/algorithms/sliding-window.ts
201
+ var SlidingWindow = class {
202
+ constructor(config) {
203
+ this.config = config;
204
+ }
205
+ /**
206
+ * Validates the sliding window configuration.
207
+ *
208
+ * Ensures the configured window size and request limit are positive values.
209
+ *
210
+ * @throws BadArgumentsException
211
+ * Thrown if:
212
+ * - `limit <= 0`
213
+ * - `window <= 0`
214
+ */
215
+ validate() {
216
+ if (this.config.limit <= 0)
217
+ throw new BadArgumentsException(
218
+ `Expected limit to be positive, got limit=${this.config.limit}`
219
+ );
220
+ if (this.config.window <= 0)
221
+ throw new BadArgumentsException(
222
+ `Expected window to be positive, got window=${this.config.window}`
223
+ );
224
+ }
225
+ };
226
+
227
+ // src/algorithms/sliding-window-counter.ts
228
+ var SlidingWindowCounter = class {
229
+ constructor(config) {
230
+ this.config = config;
231
+ }
232
+ /**
233
+ * Validates the sliding window counter configuration.
234
+ *
235
+ * Ensures the configured window size and request limit are positive values.
236
+ *
237
+ * @throws BadArgumentsException
238
+ * Thrown if:
239
+ * - `limit <= 0`
240
+ * - `window <= 0`
241
+ */
242
+ validate() {
243
+ if (this.config.limit <= 0)
244
+ throw new BadArgumentsException(
245
+ `Expected limit to be positive, got limit=${this.config.limit}`
246
+ );
247
+ if (this.config.window <= 0)
248
+ throw new BadArgumentsException(
249
+ `Expected window to be positive, got window=${this.config.window}`
250
+ );
251
+ }
252
+ };
253
+
254
+ // src/algorithms/token-bucket.ts
255
+ var TokenBucket = class {
256
+ constructor(config) {
257
+ this.config = config;
258
+ }
259
+ /**
260
+ * Validates the token bucket configuration.
261
+ *
262
+ * Ensures the configured capacity and refill rate are positive values.
263
+ *
264
+ * @throws BadArgumentsException
265
+ * Thrown if:
266
+ * - `capacity <= 0`
267
+ * - `refillRate <= 0`
268
+ */
269
+ validate() {
270
+ if (this.config.capacity <= 0)
271
+ throw new BadArgumentsException(
272
+ `Expected capacity to be positive, got capacity=${this.config.capacity}`
273
+ );
274
+ if (this.config.refillRate <= 0)
275
+ throw new BadArgumentsException(
276
+ `Expected refillRate to be positive, got refillRate=${this.config.refillRate}`
277
+ );
278
+ }
279
+ };
280
+
281
+ // src/algorithms/leaky-bucket.ts
282
+ var LeakyBucket = class {
283
+ constructor(config) {
284
+ this.config = config;
285
+ }
286
+ /**
287
+ * Validates the leaky bucket configuration.
288
+ *
289
+ * Ensures the configured capacity and leak rate are positive values.
290
+ *
291
+ * @throws BadArgumentsException
292
+ * Thrown if:
293
+ * - `capacity <= 0`
294
+ * - `leakRate <= 0`
295
+ */
296
+ validate() {
297
+ if (this.config.capacity <= 0)
298
+ throw new BadArgumentsException(
299
+ `Expected capacity to be positive, got capacity=${this.config.capacity}`
300
+ );
301
+ if (this.config.leakRate <= 0)
302
+ throw new BadArgumentsException(
303
+ `Expected leakRate to be positive, got leakRate=${this.config.leakRate}`
304
+ );
305
+ }
306
+ };
307
+
308
+ // src/algorithms/gcra.ts
309
+ var GCRA = class {
310
+ constructor(config) {
311
+ this.config = config;
312
+ }
313
+ /**
314
+ * Validates the GCRA configuration.
315
+ *
316
+ * Ensures the configured burst and interval are positive values.
317
+ *
318
+ * @throws BadArgumentsException
319
+ * Thrown if:
320
+ * - `burst <= 0`
321
+ * - `interval <= 0`
322
+ */
323
+ validate() {
324
+ if (this.config.burst <= 0)
325
+ throw new BadArgumentsException(
326
+ `Expected burst to be positive, got burst=${this.config.burst}`
327
+ );
328
+ if (this.config.interval <= 0)
329
+ throw new BadArgumentsException(
330
+ `Expected interval to be positive, got interval=${this.config.interval}`
331
+ );
332
+ }
333
+ };
334
+ export {
335
+ BadArgumentsException,
336
+ EmptyRulesException,
337
+ FixedWindow,
338
+ GCRA,
339
+ LeakyBucket,
340
+ RateLimiter,
341
+ SlidingWindow,
342
+ SlidingWindowCounter,
343
+ TokenBucket,
344
+ UnknownAlgorithmException,
345
+ addConfigToKey,
346
+ mergeRules
347
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@limitkit/core",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "module": "dist/index.mjs",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "description": "Core rate limiting engine for LimitKit",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/alphatrann/limitkit"
18
+ },
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format esm,cjs --dts",
21
+ "test": "jest --silent"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^25.4.0"
25
+ }
26
+ }