ratelimit-engine 0.0.0-stage → 1.0.1

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +437 -2
  3. package/bin/cli.js +291 -0
  4. package/package.json +56 -4
  5. package/src/adapters/BaseAdapter.js +38 -0
  6. package/src/adapters/ExpressAdapter.js +64 -0
  7. package/src/adapters/FastifyAdapter.js +67 -0
  8. package/src/algorithms/BaseAlgorithm.js +40 -0
  9. package/src/algorithms/Composite.js +97 -0
  10. package/src/algorithms/FixedWindow.js +70 -0
  11. package/src/algorithms/LeakyBucket.js +88 -0
  12. package/src/algorithms/SlidingWindow.js +99 -0
  13. package/src/algorithms/TokenBucket.js +85 -0
  14. package/src/constants/algorithms.js +18 -0
  15. package/src/constants/defaults.js +15 -0
  16. package/src/constants/errors.js +42 -0
  17. package/src/constants/headers.js +18 -0
  18. package/src/constants/store.js +20 -0
  19. package/src/engine/HeaderWriter.js +56 -0
  20. package/src/engine/KeyResolver.js +41 -0
  21. package/src/engine/RateLimiter.js +145 -0
  22. package/src/engine/createLimiter.js +37 -0
  23. package/src/errors/ConfigurationError.js +20 -0
  24. package/src/errors/ErrorFactory.js +117 -0
  25. package/src/errors/RateLimitError.js +25 -0
  26. package/src/errors/StoreError.js +20 -0
  27. package/src/index.js +52 -0
  28. package/src/logger/Logger.js +127 -0
  29. package/src/logger/LoggerFactory.js +36 -0
  30. package/src/resolvers/byAPIKey.js +11 -0
  31. package/src/resolvers/byIP.js +11 -0
  32. package/src/resolvers/byRoute.js +13 -0
  33. package/src/resolvers/byUser.js +11 -0
  34. package/src/resolvers/composite.js +18 -0
  35. package/src/stores/BaseStore.js +69 -0
  36. package/src/stores/MemoryStore.js +129 -0
  37. package/src/stores/RedisStore.js +168 -0
  38. package/src/types/index.js +79 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Abhinav710
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,438 @@
1
- # Temporary Holding Version
1
+ # ratelimit-engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Enterprise-grade, algorithm-pluggable rate limiting engine for Node.js
4
+
5
+ [![CI](https://github.com/your-org/ratelimit-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/ratelimit-engine/actions)
6
+ [![npm version](https://img.shields.io/npm/v/ratelimit-engine.svg)](https://www.npmjs.com/package/ratelimit-engine)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)](https://nodejs.org)
9
+ [![Author](https://img.shields.io/badge/author-Abhinav710-blue)](https://github.com/Abhinav710)
10
+
11
+ ---
12
+
13
+ ## Version
14
+
15
+ | Version | Status | Node.js | Release Date |
16
+ |---------|------------|----------|--------------|
17
+ | `1.0.0` | **Stable** | `>= 18` | 2024-11-12 |
18
+
19
+ ---
20
+
21
+ ## Why ratelimit-engine?
22
+
23
+ Most rate limiting libraries give you a fixed algorithm with configurable numbers.
24
+ `ratelimit-engine` treats the **algorithm as a first-class plugin** — swap it, compose it, or write your own without changing a line of engine code.
25
+
26
+ | Feature | express-rate-limit | rate-limiter-flexible | **ratelimit-engine** |
27
+ |-------------------------------|--------------------|-----------------------|----------------------|
28
+ | Pluggable algorithm | No | No | **Yes** |
29
+ | Compose multiple algorithms | No | No | **Yes** |
30
+ | Pluggable store | Partial | Yes | **Yes** |
31
+ | Structured JSON logging | No | No | **Yes** |
32
+ | Custom error factory | No | No | **Yes** |
33
+ | Framework-agnostic core | No | Partial | **Yes** |
34
+ | SOLID architecture | No | No | **Yes** |
35
+
36
+ ---
37
+
38
+ ## Features
39
+
40
+ - **5 built-in algorithms** — Fixed Window, Sliding Window, Token Bucket, Leaky Bucket, Composite
41
+ - **Composite mode** — combine algorithms with `ALL` (every must pass) or `ANY` (one must pass)
42
+ - **Pluggable stores** — in-memory (dev/test) and Redis with atomic Lua scripts (production)
43
+ - **Pluggable algorithms** — implement `BaseAlgorithm` to ship your own logic
44
+ - **Key resolvers** — by IP, user ID, API key, route, or any combination
45
+ - **Structured logging** — JSON logs with pluggable transport (Winston, Pino, or default stdout)
46
+ - **Error factory** — every error has a machine-readable code, human message, and context
47
+ - **Framework adapters** — Express and Fastify out of the box
48
+ - **Standard HTTP headers** — `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After`
49
+ - **Fail-open / fail-closed** — configurable behaviour when the store is unavailable
50
+ - **Lifecycle hooks** — `onBlocked` and `onAllowed` callbacks for observability
51
+ - **Zero magic values** — all constants in dedicated files, JSDoc types on every contract
52
+
53
+ ---
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ npm install ratelimit-engine
59
+
60
+ # Redis store requires ioredis (peer dependency)
61
+ npm install ioredis
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Quick Start
67
+
68
+ ```js
69
+ import { createLimiter, TokenBucket, MemoryStore, byIP } from 'ratelimit-engine'
70
+ import express from 'express'
71
+
72
+ const app = express()
73
+
74
+ const limiter = createLimiter({
75
+ algorithm: new TokenBucket({ capacity: 100, refillRate: 10 }),
76
+ store: new MemoryStore(),
77
+ keyBy: byIP,
78
+ })
79
+
80
+ app.use(limiter.express())
81
+
82
+ app.get('/', (_req, res) => res.json({ message: 'ok' }))
83
+ app.listen(3000)
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Architecture
89
+
90
+ ```
91
+ ┌─────────────────────────────────────────────────────────────┐
92
+ │ Your Application │
93
+ └──────────────────────────┬──────────────────────────────────┘
94
+ │
95
+ ┌──────────────────────────▼──────────────────────────────────┐
96
+ │ Framework Adapters │
97
+ │ ExpressAdapter · FastifyAdapter │
98
+ │ Extracts RequestContext from req object │
99
+ └──────────────────────────┬──────────────────────────────────┘
100
+ │ RequestContext
101
+ ┌──────────────────────────▼──────────────────────────────────┐
102
+ │ Engine Core │
103
+ │ │
104
+ │ KeyResolver RateLimiter HeaderWriter │
105
+ │ resolves key ──► orchestrates ──► writes headers │
106
+ │ from context check/reset on response │
107
+ └───────────┬────────────────────┬────────────────────────────┘
108
+ │ │
109
+ ┌─────────▼──────┐ ┌─────────▼──────────────────────────┐
110
+ │ Algorithm Layer│ │ Store Layer │
111
+ │ │ │ │
112
+ │ BaseAlgorithm │ │ BaseStore │
113
+ │ FixedWindow │ │ MemoryStore (dev / single node) │
114
+ │ SlidingWindow │ │ RedisStore (prod / distributed) │
115
+ │ TokenBucket │ │ │
116
+ │ LeakyBucket │ │ Redis uses atomic Lua scripts — │
117
+ │ Composite │ │ no race conditions across pods │
118
+ │ [Your own] │ │ [Your own store] │
119
+ └────────────────┘ └─────────────────────────────────────┘
120
+ ```
121
+
122
+ ### Request Flow
123
+
124
+ ```
125
+ Incoming Request
126
+ │
127
+ ▼
128
+ Adapter.extractContext(req)
129
+ │ builds RequestContext {ip, userId, apiKey, method, path, headers}
130
+ ▼
131
+ KeyResolver.resolve(context)
132
+ │ applies keyBy fn → prefixed string key e.g. "rle:ip:1.2.3.4"
133
+ ▼
134
+ Algorithm.check(key, store)
135
+ │ reads/writes store, applies algorithm math
136
+ │ returns RateLimitResult {allowed, limit, remaining, resetAt, retryAfter}
137
+ ▼
138
+ HeaderWriter.write(res, result)
139
+ │ sets X-RateLimit-* headers
140
+ ▼
141
+ allowed?
142
+ ├── YES → next() / continue
143
+ └── NO → HeaderWriter.sendBlocked() → 429 + Retry-After
144
+ │
145
+ ▼
146
+ onBlocked hook (optional)
147
+ ```
148
+
149
+ ### Module Dependency Map
150
+
151
+ ```
152
+ index.js
153
+ ├── engine/createLimiter.js (factory)
154
+ │ └── engine/RateLimiter.js
155
+ │ ├── engine/KeyResolver.js
156
+ │ ├── engine/HeaderWriter.js
157
+ │ ├── algorithms/BaseAlgorithm.js
158
+ │ └── stores/BaseStore.js
159
+ ├── algorithms/*
160
+ ├── stores/*
161
+ ├── adapters/*
162
+ ├── resolvers/*
163
+ ├── logger/Logger.js + LoggerFactory.js
164
+ ├── errors/ErrorFactory.js ← single source of all errors
165
+ ├── constants/* ← single source of all strings/numbers
166
+ └── types/index.js ← single source of all JSDoc contracts
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Algorithms
172
+
173
+ ### Fixed Window
174
+
175
+ Counts requests in a fixed time slot. Simple and cheap. Boundary bursts are possible.
176
+
177
+ ```js
178
+ import { FixedWindow } from 'ratelimit-engine'
179
+
180
+ new FixedWindow({ limit: 100, windowMs: 60_000 })
181
+ // 100 requests per minute
182
+ ```
183
+
184
+ ### Sliding Window
185
+
186
+ Weighted approximation of a rolling window. No boundary spike problem.
187
+
188
+ ```js
189
+ import { SlidingWindow } from 'ratelimit-engine'
190
+
191
+ new SlidingWindow({ limit: 100, windowMs: 60_000 })
192
+ ```
193
+
194
+ ### Token Bucket
195
+
196
+ Tokens refill at a fixed rate. Allows controlled bursting.
197
+
198
+ ```js
199
+ import { TokenBucket } from 'ratelimit-engine'
200
+
201
+ new TokenBucket({ capacity: 100, refillRate: 10 })
202
+ // bucket holds 100 tokens, refills 10/sec
203
+ ```
204
+
205
+ ### Leaky Bucket
206
+
207
+ Requests processed at a fixed rate. Excess dropped. Strict output rate control.
208
+
209
+ ```js
210
+ import { LeakyBucket } from 'ratelimit-engine'
211
+
212
+ new LeakyBucket({ capacity: 100, leakRate: 10 })
213
+ // queue depth 100, drains 10 requests/sec
214
+ ```
215
+
216
+ ### Composite
217
+
218
+ Combines multiple algorithms. Use `ALL` (every must pass) or `ANY` (one must pass).
219
+
220
+ ```js
221
+ import { Composite, TokenBucket, SlidingWindow, COMPOSITE_MODES } from 'ratelimit-engine'
222
+
223
+ // Allow burst but enforce a per-minute hard cap
224
+ new Composite({
225
+ algorithms: [
226
+ new TokenBucket({ capacity: 50, refillRate: 10 }),
227
+ new SlidingWindow({ limit: 200, windowMs: 60_000 }),
228
+ ],
229
+ mode: COMPOSITE_MODES.ALL,
230
+ })
231
+ ```
232
+
233
+ ### Custom Algorithm
234
+
235
+ Implement `BaseAlgorithm` to plug in your own logic:
236
+
237
+ ```js
238
+ import { BaseAlgorithm } from 'ratelimit-engine'
239
+
240
+ class MyAlgorithm extends BaseAlgorithm {
241
+ constructor() {
242
+ super('my-algorithm')
243
+ }
244
+
245
+ async check(key, store) {
246
+ // your logic here
247
+ // store.get / store.set / store.increment available
248
+ return {
249
+ allowed: true,
250
+ limit: 100,
251
+ remaining: 99,
252
+ resetAt: new Date(Date.now() + 60_000),
253
+ }
254
+ }
255
+
256
+ async reset(key, store) {
257
+ await store.delete(key)
258
+ }
259
+ }
260
+ ```
261
+
262
+ ---
263
+
264
+ ## Stores
265
+
266
+ ### MemoryStore
267
+
268
+ Single-instance, in-process. Best for development and testing.
269
+
270
+ ```js
271
+ import { MemoryStore } from 'ratelimit-engine'
272
+
273
+ new MemoryStore()
274
+ ```
275
+
276
+ ### RedisStore
277
+
278
+ Distributed, atomic Lua scripts. Required for multi-pod / Kubernetes deployments.
279
+
280
+ ```js
281
+ import { RedisStore } from 'ratelimit-engine'
282
+ import Redis from 'ioredis'
283
+
284
+ const redis = new Redis({ host: 'localhost', port: 6379 })
285
+
286
+ new RedisStore({ client: redis, prefix: 'myapp:rl:' })
287
+ ```
288
+
289
+ ### Custom Store
290
+
291
+ Implement `BaseStore` to use any backend (Postgres, Valkey, DragonflyDB, etc.):
292
+
293
+ ```js
294
+ import { BaseStore } from 'ratelimit-engine'
295
+
296
+ class PostgresStore extends BaseStore {
297
+ constructor(pool) {
298
+ super('postgres')
299
+ this._pool = pool
300
+ }
301
+
302
+ async get(key) { /* ... */ }
303
+ async set(key, entry, ttlMs) { /* ... */ }
304
+ async increment(key, ttlMs) { /* ... */ }
305
+ async delete(key) { /* ... */ }
306
+ }
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Key Resolvers
312
+
313
+ ```js
314
+ import { byIP, byUser, byAPIKey, byRoute, compositeResolver } from 'ratelimit-engine'
315
+
316
+ byIP // "ip:1.2.3.4"
317
+ byUser // "user:abc123"
318
+ byAPIKey // "apikey:sk-..."
319
+ byRoute // "route:GET:/api/data"
320
+
321
+ // Combine resolvers — scopes limit to user + route together
322
+ compositeResolver([byUser, byRoute]) // "user:abc123|route:GET:/api/data"
323
+
324
+ // Custom resolver
325
+ const byTenant = (ctx) => `tenant:${ctx.rawRequest.headers['x-tenant-id']}`
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Framework Adapters
331
+
332
+ ### Express
333
+
334
+ ```js
335
+ const limiter = createLimiter({ algorithm, store, keyBy })
336
+
337
+ app.use(limiter.express()) // global
338
+ app.use('/api', limiter.express()) // route-scoped
339
+ ```
340
+
341
+ ### Fastify
342
+
343
+ ```js
344
+ fastify.addHook('onRequest', limiter.fastify())
345
+ ```
346
+
347
+ ### Framework-agnostic
348
+
349
+ ```js
350
+ const result = await limiter.check({
351
+ ip: '1.2.3.4',
352
+ userId: 'user:99',
353
+ method: 'GET',
354
+ path: '/api/data',
355
+ headers: {},
356
+ })
357
+
358
+ if (!result.allowed) {
359
+ // handle 429 yourself
360
+ }
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Logging
366
+
367
+ Default output is structured JSON to stdout/stderr. Plug in Winston or Pino:
368
+
369
+ ```js
370
+ import { createLimiter, wrapExternalLogger } from 'ratelimit-engine'
371
+ import winston from 'winston'
372
+
373
+ const logger = winston.createLogger({ /* ... */ })
374
+
375
+ const limiter = createLimiter({
376
+ algorithm, store, keyBy,
377
+ logger: wrapExternalLogger(logger),
378
+ })
379
+ ```
380
+
381
+ Log levels: `debug` → `info` → `warn` → `error`
382
+
383
+ ---
384
+
385
+ ## Error Handling
386
+
387
+ Every error has a machine-readable `code` from `ERROR_CODES`:
388
+
389
+ ```js
390
+ import { ErrorFactory, ERROR_CODES } from 'ratelimit-engine'
391
+
392
+ try {
393
+ await limiter.check(context)
394
+ } catch (err) {
395
+ if (err.code === ERROR_CODES.STORE_UNAVAILABLE) {
396
+ // handle store outage
397
+ }
398
+ }
399
+ ```
400
+
401
+ ---
402
+
403
+ ## API Reference
404
+
405
+ ### `createLimiter(config)`
406
+
407
+ | Field | Type | Required | Default | Description |
408
+ |--------------|-------------------|----------|---------|------------------------------------------|
409
+ | `algorithm` | `BaseAlgorithm` | Yes | — | Algorithm instance |
410
+ | `store` | `BaseStore` | Yes | — | Store instance |
411
+ | `keyBy` | `Function` | Yes | — | Key resolver function |
412
+ | `failOpen` | `boolean` | No | `true` | Allow requests when store is unavailable |
413
+ | `onBlocked` | `Function` | No | — | Hook called when request is blocked |
414
+ | `onAllowed` | `Function` | No | — | Hook called when request is allowed |
415
+ | `logger` | `LoggerInterface` | No | — | Custom logger instance |
416
+
417
+ Returns `{ limiter, express(), fastify(), check(), reset(), destroy() }`
418
+
419
+ ---
420
+
421
+ ## Examples
422
+
423
+ ```bash
424
+ node examples/express-basic/index.js # Token Bucket + MemoryStore + byIP
425
+ node examples/express-composite/index.js # Composite + MemoryStore + byUser
426
+ ```
427
+
428
+ ---
429
+
430
+ ## Contributing
431
+
432
+ See [CONTRIBUTING.md](./CONTRIBUTING.md).
433
+
434
+ ---
435
+
436
+ ## License
437
+
438
+ [MIT](./LICENSE)