@limitkit/core 0.1.5 β 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -64
- package/dist/index.js +3 -8
- package/dist/index.mjs +3 -8
- package/package.json +43 -29
package/README.md
CHANGED
|
@@ -1,117 +1,157 @@
|
|
|
1
|
-
#
|
|
1
|
+
# π¦ `@limitkit/core`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@limitkit/core)
|
|
4
|
+
[](https://www.npmjs.com/package/@limitkit/core)
|
|
5
|
+
[](https://github.com/alphatrann/limitkit/blob/main/LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
π Main project: https://github.com/alphatrann/limitkit
|
|
7
|
+
**A policy-driven rate limiting engine built on composable rules.**
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
# π Works With
|
|
12
|
+
|
|
13
|
+
The core engine is designed to integrate with:
|
|
14
|
+
|
|
15
|
+
* `@limitkit/memory` β in-memory store
|
|
16
|
+
* `@limitkit/redis` β distributed rate limiting
|
|
17
|
+
* `@limitkit/express` β middleware
|
|
18
|
+
* `@limitkit/nest` β guard + decorators
|
|
12
19
|
|
|
13
|
-
```bash
|
|
14
|
-
npm install @limitkit/core
|
|
15
|
-
````
|
|
16
20
|
|
|
17
21
|
---
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
# β‘ Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install @limitkit/core
|
|
27
|
+
```
|
|
20
28
|
|
|
21
29
|
```ts
|
|
22
|
-
import { RateLimiter } from "@limitkit/core"
|
|
23
|
-
import { InMemoryStore, InMemoryFixedWindow } from "@limitkit/memory"
|
|
30
|
+
import { RateLimiter } from "@limitkit/core";
|
|
24
31
|
|
|
25
32
|
const limiter = new RateLimiter({
|
|
26
|
-
store
|
|
33
|
+
store,
|
|
27
34
|
rules: [
|
|
28
35
|
{
|
|
29
36
|
name: "global",
|
|
30
|
-
key:
|
|
31
|
-
policy:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
key: "global",
|
|
38
|
+
policy: ...,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const result = await limiter.consume(ctx);
|
|
44
|
+
|
|
45
|
+
if (!result.allowed) {
|
|
46
|
+
console.log("Rate limited");
|
|
47
|
+
}
|
|
39
48
|
```
|
|
40
49
|
|
|
41
50
|
---
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
# π§ Core Idea
|
|
44
53
|
|
|
45
|
-
|
|
54
|
+
Most rate limiters answer:
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
* Sliding Window
|
|
49
|
-
* Sliding Window Counter
|
|
50
|
-
* Token Bucket
|
|
51
|
-
* Leaky Bucket
|
|
52
|
-
* GCRA
|
|
56
|
+
> βHow many requests per IP?β
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
LimitKit answers:
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
> **βWhat rules should control this request?β**
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
```ts
|
|
63
|
+
global β ip β user β endpoint
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Rules run top β bottom and stop on first failure.
|
|
67
|
+
|
|
68
|
+
---
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
# π§© Concepts
|
|
61
71
|
|
|
62
|
-
|
|
72
|
+
A **rule** defines *who*, *how*, and *how much*:
|
|
63
73
|
|
|
64
74
|
```ts
|
|
65
75
|
{
|
|
66
|
-
name: "
|
|
67
|
-
key: (
|
|
68
|
-
policy: new
|
|
69
|
-
|
|
70
|
-
window: 60,
|
|
71
|
-
limit: 100
|
|
72
|
-
})
|
|
76
|
+
name: "user",
|
|
77
|
+
key: (ctx) => ctx.user.id,
|
|
78
|
+
policy: new TokenBucket(...),
|
|
79
|
+
cost: 1
|
|
73
80
|
}
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
* **key** β groups requests (string, function, or async)
|
|
84
|
+
* **policy** β rate limiting algorithm (fixed, sliding, token bucket)
|
|
85
|
+
* **cost** β weight per request (default: 1)
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
Policies can also be dynamic:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
policy: (ctx) => {
|
|
91
|
+
return ctx.user.plan === "pro" ? proPolicy : freePolicy;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
81
94
|
|
|
82
95
|
---
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
# π― Examples
|
|
85
98
|
|
|
86
|
-
|
|
99
|
+
## Layered Limits
|
|
87
100
|
|
|
88
101
|
```ts
|
|
89
|
-
|
|
102
|
+
rules: [
|
|
103
|
+
{ name: "global", key: "global", policy: ... },
|
|
104
|
+
{ name: "ip", key: (ctx) => ctx.ip, policy: ... },
|
|
105
|
+
{ name: "user", key: (ctx) => ctx.user.id, policy: ... },
|
|
106
|
+
]
|
|
107
|
+
```
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
## SaaS Plans
|
|
92
110
|
|
|
93
|
-
|
|
111
|
+
```ts
|
|
112
|
+
{
|
|
113
|
+
key: (ctx) => ctx.user.id,
|
|
114
|
+
policy: (ctx) => {
|
|
115
|
+
return ctx.user.plan === "pro" ? proPolicy : freePolicy;
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
```
|
|
94
119
|
|
|
95
|
-
|
|
96
|
-
if (this.config.limit <= 0) {
|
|
97
|
-
throw new Error("Invalid configuration")
|
|
98
|
-
}
|
|
99
|
-
}
|
|
120
|
+
## Expensive Operations
|
|
100
121
|
|
|
122
|
+
```ts
|
|
123
|
+
{
|
|
124
|
+
key: (ctx) => ctx.user.id,
|
|
125
|
+
cost: (ctx) => ctx.endpoint === "/report" ? 10 : 1,
|
|
126
|
+
policy: tokenBucket,
|
|
101
127
|
}
|
|
102
128
|
```
|
|
103
129
|
|
|
104
130
|
---
|
|
105
131
|
|
|
106
|
-
|
|
132
|
+
# π Result
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
{
|
|
136
|
+
allowed: boolean,
|
|
137
|
+
limit: number,
|
|
138
|
+
remaining: number,
|
|
139
|
+
reset: number,
|
|
140
|
+
retryAfter?: number
|
|
141
|
+
}
|
|
142
|
+
```
|
|
107
143
|
|
|
108
|
-
|
|
144
|
+
* **allowed** β request permitted or blocked
|
|
145
|
+
* **limit** β max allowed requests
|
|
146
|
+
* **remaining** β remaining quota
|
|
147
|
+
* **reset** β timestamp (ms) when fully reset
|
|
148
|
+
* **retryAfter** β seconds to wait (if blocked)
|
|
109
149
|
|
|
110
|
-
|
|
150
|
+
---
|
|
111
151
|
|
|
112
|
-
|
|
152
|
+
# π Summary
|
|
113
153
|
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
154
|
+
* Rule-based rate limiting
|
|
155
|
+
* Dynamic, context-aware policies
|
|
156
|
+
* Weighted requests
|
|
157
|
+
* Early exit for performance
|
package/dist/index.js
CHANGED
|
@@ -154,7 +154,7 @@ var RateLimiter = class {
|
|
|
154
154
|
const algorithm = typeof rule.policy === "function" ? await rule.policy(ctx) : rule.policy;
|
|
155
155
|
const key = typeof rule.key === "function" ? await rule.key(ctx) : rule.key;
|
|
156
156
|
const cost = typeof rule.cost === "function" ? await rule.cost(ctx) : rule.cost;
|
|
157
|
-
if (cost && cost <= 0)
|
|
157
|
+
if (cost !== void 0 && cost <= 0)
|
|
158
158
|
throw new BadArgumentsException(
|
|
159
159
|
`Cost must be a positive integer, got cost=${cost}`
|
|
160
160
|
);
|
|
@@ -173,7 +173,7 @@ var RateLimiter = class {
|
|
|
173
173
|
if (result.allowed) console.log(debugRules);
|
|
174
174
|
else console.error(debugRules);
|
|
175
175
|
}
|
|
176
|
-
if (result.
|
|
176
|
+
if (!result.allowed) {
|
|
177
177
|
if (this.debug) {
|
|
178
178
|
const debugResults = {
|
|
179
179
|
failedRule: rule.name,
|
|
@@ -182,12 +182,7 @@ var RateLimiter = class {
|
|
|
182
182
|
};
|
|
183
183
|
return debugResults;
|
|
184
184
|
}
|
|
185
|
-
return
|
|
186
|
-
...result,
|
|
187
|
-
reset: maxReset,
|
|
188
|
-
limit: minLimit,
|
|
189
|
-
remaining: minRemaining
|
|
190
|
-
};
|
|
185
|
+
return result;
|
|
191
186
|
}
|
|
192
187
|
}
|
|
193
188
|
if (this.debug) {
|
package/dist/index.mjs
CHANGED
|
@@ -117,7 +117,7 @@ var RateLimiter = class {
|
|
|
117
117
|
const algorithm = typeof rule.policy === "function" ? await rule.policy(ctx) : rule.policy;
|
|
118
118
|
const key = typeof rule.key === "function" ? await rule.key(ctx) : rule.key;
|
|
119
119
|
const cost = typeof rule.cost === "function" ? await rule.cost(ctx) : rule.cost;
|
|
120
|
-
if (cost && cost <= 0)
|
|
120
|
+
if (cost !== void 0 && cost <= 0)
|
|
121
121
|
throw new BadArgumentsException(
|
|
122
122
|
`Cost must be a positive integer, got cost=${cost}`
|
|
123
123
|
);
|
|
@@ -136,7 +136,7 @@ var RateLimiter = class {
|
|
|
136
136
|
if (result.allowed) console.log(debugRules);
|
|
137
137
|
else console.error(debugRules);
|
|
138
138
|
}
|
|
139
|
-
if (result.
|
|
139
|
+
if (!result.allowed) {
|
|
140
140
|
if (this.debug) {
|
|
141
141
|
const debugResults = {
|
|
142
142
|
failedRule: rule.name,
|
|
@@ -145,12 +145,7 @@ var RateLimiter = class {
|
|
|
145
145
|
};
|
|
146
146
|
return debugResults;
|
|
147
147
|
}
|
|
148
|
-
return
|
|
149
|
-
...result,
|
|
150
|
-
reset: maxReset,
|
|
151
|
-
limit: minLimit,
|
|
152
|
-
remaining: minRemaining
|
|
153
|
-
};
|
|
148
|
+
return result;
|
|
154
149
|
}
|
|
155
150
|
}
|
|
156
151
|
if (this.debug) {
|
package/package.json
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@limitkit/core",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"module": "dist/index.mjs",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@limitkit/core",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"rate-limiter",
|
|
9
|
+
"rate-limit",
|
|
10
|
+
"rate-limiting",
|
|
11
|
+
"throttling",
|
|
12
|
+
"token-bucket",
|
|
13
|
+
"leaky-bucket",
|
|
14
|
+
"sliding-window",
|
|
15
|
+
"sliding-window-counter",
|
|
16
|
+
"gcra",
|
|
17
|
+
"rules-engine",
|
|
18
|
+
"policy-based",
|
|
19
|
+
"nodejs"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"description": "Core rate limiting engine for LimitKit",
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/alphatrann/limitkit"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
38
|
+
"test": "jest --silent"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.4.0"
|
|
42
|
+
}
|
|
43
|
+
}
|