@joint-ops/hitlimit 1.0.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +396 -0
  3. package/dist/core/config.d.ts +3 -0
  4. package/dist/core/config.d.ts.map +1 -0
  5. package/dist/core/config.js +20 -0
  6. package/dist/core/config.js.map +1 -0
  7. package/dist/core/headers.d.ts +3 -0
  8. package/dist/core/headers.d.ts.map +1 -0
  9. package/dist/core/headers.js +18 -0
  10. package/dist/core/headers.js.map +1 -0
  11. package/dist/core/limiter.d.ts +3 -0
  12. package/dist/core/limiter.d.ts.map +1 -0
  13. package/dist/core/limiter.js +48 -0
  14. package/dist/core/limiter.js.map +1 -0
  15. package/dist/core/response.d.ts +3 -0
  16. package/dist/core/response.d.ts.map +1 -0
  17. package/dist/core/response.js +12 -0
  18. package/dist/core/response.js.map +1 -0
  19. package/dist/core/utils.d.ts +3 -0
  20. package/dist/core/utils.d.ts.map +1 -0
  21. package/dist/core/utils.js +19 -0
  22. package/dist/core/utils.js.map +1 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +40 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/loggers/console.d.ts +4 -0
  28. package/dist/loggers/console.d.ts.map +1 -0
  29. package/dist/loggers/console.js +17 -0
  30. package/dist/loggers/console.js.map +1 -0
  31. package/dist/loggers/pino.d.ts +4 -0
  32. package/dist/loggers/pino.d.ts.map +1 -0
  33. package/dist/loggers/pino.js +10 -0
  34. package/dist/loggers/pino.js.map +1 -0
  35. package/dist/loggers/winston.d.ts +4 -0
  36. package/dist/loggers/winston.d.ts.map +1 -0
  37. package/dist/loggers/winston.js +9 -0
  38. package/dist/loggers/winston.js.map +1 -0
  39. package/dist/nest.d.ts +27 -0
  40. package/dist/nest.d.ts.map +1 -0
  41. package/dist/nest.js +114 -0
  42. package/dist/nest.js.map +1 -0
  43. package/dist/node.d.ts +10 -0
  44. package/dist/node.d.ts.map +1 -0
  45. package/dist/node.js +50 -0
  46. package/dist/node.js.map +1 -0
  47. package/dist/stores/memory.d.ts +3 -0
  48. package/dist/stores/memory.d.ts.map +1 -0
  49. package/dist/stores/memory.js +37 -0
  50. package/dist/stores/memory.js.map +1 -0
  51. package/dist/stores/redis.d.ts +7 -0
  52. package/dist/stores/redis.d.ts.map +1 -0
  53. package/dist/stores/redis.js +36 -0
  54. package/dist/stores/redis.js.map +1 -0
  55. package/dist/stores/sqlite.d.ts +6 -0
  56. package/dist/stores/sqlite.d.ts.map +1 -0
  57. package/dist/stores/sqlite.js +48 -0
  58. package/dist/stores/sqlite.js.map +1 -0
  59. package/package.json +160 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 JointOps
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 ADDED
@@ -0,0 +1,396 @@
1
+ # @joint-ops/hitlimit
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@joint-ops/hitlimit.svg)](https://www.npmjs.com/package/@joint-ops/hitlimit)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@joint-ops/hitlimit.svg)](https://www.npmjs.com/package/@joint-ops/hitlimit)
5
+ [![GitHub](https://img.shields.io/github/license/JointOps/hitlimit-monorepo)](https://github.com/JointOps/hitlimit-monorepo)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@joint-ops/hitlimit)](https://bundlephobia.com/package/@joint-ops/hitlimit)
8
+
9
+ > The fastest rate limiter for Node.js - Express, NestJS, and native HTTP | express-rate-limit alternative
10
+
11
+ **hitlimit** is a high-performance rate limiting middleware for Node.js applications. Protect your APIs from abuse, prevent brute force attacks, and throttle requests with sub-millisecond overhead. A faster, lighter alternative to express-rate-limit and rate-limiter-flexible.
12
+
13
+ **[Documentation](https://hitlimit.dev)** | **[GitHub](https://github.com/JointOps/hitlimit-monorepo)** | **[npm](https://www.npmjs.com/package/@joint-ops/hitlimit)**
14
+
15
+ ## Why hitlimit?
16
+
17
+ - **Blazing Fast** - 400,000+ ops/sec with memory store, ~7% HTTP overhead
18
+ - **Zero Config** - Works out of the box with sensible defaults
19
+ - **Tiny Footprint** - Only ~5KB minified, no bloat
20
+ - **Framework Agnostic** - Express, NestJS, Fastify, native HTTP
21
+ - **Multiple Stores** - Memory, Redis, SQLite for distributed systems
22
+ - **TypeScript First** - Full type safety and IntelliSense support
23
+ - **Flexible Keys** - Rate limit by IP, user ID, API key, or custom logic
24
+ - **Tiered Limits** - Different limits for free/pro/enterprise users
25
+ - **Standard Headers** - RFC-compliant RateLimit-* and X-RateLimit-* headers
26
+
27
+ ## Performance
28
+
29
+ hitlimit is designed for speed. Here's how it performs:
30
+
31
+ ### Store Benchmarks
32
+
33
+ | Store | Operations/sec | Avg Latency | Use Case |
34
+ |-------|----------------|-------------|----------|
35
+ | **Memory** | 400,000+ | 0.002ms | Single instance, no persistence |
36
+ | **SQLite** | 35,000+ | 0.025ms | Single instance, persistence needed |
37
+ | **Redis** | 12,000+ | 0.08ms | Multi-instance, distributed |
38
+
39
+ ### vs Competitors
40
+
41
+ | Library | Memory (ops/s) | Bundle Size |
42
+ |---------|----------------|-------------|
43
+ | **hitlimit** | **400,000** | **~5KB** |
44
+ | rate-limiter-flexible | 250,000 | ~45KB |
45
+ | express-rate-limit | 180,000 | ~15KB |
46
+
47
+ ### HTTP Overhead
48
+
49
+ | Framework | Without Limiter | With hitlimit | Overhead |
50
+ |-----------|-----------------|---------------|----------|
51
+ | Express | 45,000 req/s | 42,000 req/s | **7%** |
52
+ | Fastify | 65,000 req/s | 61,000 req/s | **6%** |
53
+
54
+ <details>
55
+ <summary>Run benchmarks yourself</summary>
56
+
57
+ ```bash
58
+ git clone https://github.com/JointOps/hitlimit-monorepo
59
+ cd hitlimit-monorepo
60
+ pnpm install
61
+ pnpm build
62
+ pnpm benchmark
63
+ ```
64
+
65
+ </details>
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ npm install @joint-ops/hitlimit
71
+ # or
72
+ pnpm add @joint-ops/hitlimit
73
+ # or
74
+ yarn add @joint-ops/hitlimit
75
+ ```
76
+
77
+ ## Quick Start
78
+
79
+ ### Express Rate Limiting
80
+
81
+ ```javascript
82
+ import express from 'express'
83
+ import { hitlimit } from '@joint-ops/hitlimit'
84
+
85
+ const app = express()
86
+
87
+ // Default: 100 requests per minute per IP
88
+ app.use(hitlimit())
89
+
90
+ // Or with custom options
91
+ app.use(hitlimit({
92
+ limit: 100, // max requests
93
+ window: '15m' // per 15 minutes
94
+ }))
95
+
96
+ app.get('/api', (req, res) => res.json({ status: 'ok' }))
97
+ app.listen(3000)
98
+ ```
99
+
100
+ ### NestJS Rate Limiting
101
+
102
+ ```typescript
103
+ import { Module } from '@nestjs/common'
104
+ import { APP_GUARD } from '@nestjs/core'
105
+ import { HitLimitModule, HitLimitGuard } from '@joint-ops/hitlimit/nest'
106
+
107
+ @Module({
108
+ imports: [
109
+ HitLimitModule.register({
110
+ limit: 100,
111
+ window: '1m'
112
+ })
113
+ ],
114
+ providers: [
115
+ {
116
+ provide: APP_GUARD,
117
+ useClass: HitLimitGuard
118
+ }
119
+ ]
120
+ })
121
+ export class AppModule {}
122
+ ```
123
+
124
+ ### Node.js HTTP Rate Limiting
125
+
126
+ ```javascript
127
+ import http from 'node:http'
128
+ import { createHitLimit } from '@joint-ops/hitlimit/node'
129
+
130
+ const limiter = createHitLimit({ limit: 100, window: '1m' })
131
+
132
+ const server = http.createServer(async (req, res) => {
133
+ const result = await limiter(req, res)
134
+ if (!result.allowed) return // Already sent 429
135
+
136
+ res.writeHead(200)
137
+ res.end('Hello!')
138
+ })
139
+
140
+ server.listen(3000)
141
+ ```
142
+
143
+ ## Features
144
+
145
+ ### API Rate Limiting
146
+
147
+ Protect your REST APIs from abuse and ensure fair usage across all clients.
148
+
149
+ ```javascript
150
+ // Limit API endpoints
151
+ app.use('/api', hitlimit({ limit: 1000, window: '1h' }))
152
+ ```
153
+
154
+ ### Login & Authentication Protection
155
+
156
+ Prevent brute force attacks on login endpoints with strict rate limits.
157
+
158
+ ```javascript
159
+ // Strict limits for auth routes
160
+ app.use('/auth/login', hitlimit({ limit: 5, window: '15m' }))
161
+ app.use('/auth/register', hitlimit({ limit: 3, window: '1h' }))
162
+ ```
163
+
164
+ ### Tiered Rate Limits
165
+
166
+ Different limits for different user tiers (free, pro, enterprise).
167
+
168
+ ```javascript
169
+ hitlimit({
170
+ tiers: {
171
+ free: { limit: 100, window: '1h' },
172
+ pro: { limit: 5000, window: '1h' },
173
+ enterprise: { limit: Infinity }
174
+ },
175
+ tier: (req) => req.user?.plan || 'free'
176
+ })
177
+ ```
178
+
179
+ ### Custom Rate Limit Keys
180
+
181
+ Rate limit by IP address, user ID, API key, or any custom identifier.
182
+
183
+ ```javascript
184
+ hitlimit({
185
+ key: (req) => {
186
+ // By API key
187
+ if (req.headers['x-api-key']) return req.headers['x-api-key']
188
+ // By user ID
189
+ if (req.user?.id) return `user:${req.user.id}`
190
+ // Fallback to IP
191
+ return req.ip
192
+ }
193
+ })
194
+ ```
195
+
196
+ ### Skip Certain Requests
197
+
198
+ Whitelist health checks, internal routes, or admin users.
199
+
200
+ ```javascript
201
+ hitlimit({
202
+ skip: (req) => {
203
+ if (req.path === '/health') return true
204
+ if (req.user?.role === 'admin') return true
205
+ return false
206
+ }
207
+ })
208
+ ```
209
+
210
+ ## Configuration Options
211
+
212
+ ```javascript
213
+ hitlimit({
214
+ // Basic options
215
+ limit: 100, // Max requests per window (default: 100)
216
+ window: '1m', // Time window: 30s, 15m, 1h, 1d (default: '1m')
217
+
218
+ // Custom key extraction
219
+ key: (req) => req.ip,
220
+
221
+ // Tiered rate limits
222
+ tiers: {
223
+ free: { limit: 100, window: '1h' },
224
+ pro: { limit: 5000, window: '1h' },
225
+ enterprise: { limit: Infinity }
226
+ },
227
+ tier: (req) => req.user?.plan || 'free',
228
+
229
+ // Custom 429 response
230
+ response: {
231
+ message: 'Too many requests',
232
+ statusCode: 429
233
+ },
234
+
235
+ // Headers configuration
236
+ headers: {
237
+ standard: true, // RateLimit-* headers
238
+ legacy: true, // X-RateLimit-* headers
239
+ retryAfter: true // Retry-After header on 429
240
+ },
241
+
242
+ // Store backend
243
+ store: memoryStore(),
244
+
245
+ // Skip rate limiting
246
+ skip: (req) => req.path === '/health',
247
+
248
+ // Error handling
249
+ onStoreError: (error, req) => 'allow' // or 'deny'
250
+ })
251
+ ```
252
+
253
+ ## Storage Backends
254
+
255
+ ### Memory Store (Default)
256
+
257
+ Best for single-server deployments. Fast and zero-config.
258
+
259
+ ```javascript
260
+ import { hitlimit } from '@joint-ops/hitlimit'
261
+
262
+ app.use(hitlimit()) // Uses memory store by default
263
+ ```
264
+
265
+ ### Redis Store
266
+
267
+ Best for distributed systems and multi-server deployments.
268
+
269
+ ```javascript
270
+ import { hitlimit } from '@joint-ops/hitlimit'
271
+ import { redisStore } from '@joint-ops/hitlimit/stores/redis'
272
+
273
+ app.use(hitlimit({
274
+ store: redisStore({ url: 'redis://localhost:6379' })
275
+ }))
276
+ ```
277
+
278
+ ### SQLite Store
279
+
280
+ Best for persistent rate limiting with local storage.
281
+
282
+ ```javascript
283
+ import { hitlimit } from '@joint-ops/hitlimit'
284
+ import { sqliteStore } from '@joint-ops/hitlimit/stores/sqlite'
285
+
286
+ app.use(hitlimit({
287
+ store: sqliteStore({ path: './ratelimit.db' })
288
+ }))
289
+ ```
290
+
291
+ ## Response Headers
292
+
293
+ Every response includes rate limit information:
294
+
295
+ ```
296
+ RateLimit-Limit: 100
297
+ RateLimit-Remaining: 99
298
+ RateLimit-Reset: 1234567890
299
+ X-RateLimit-Limit: 100
300
+ X-RateLimit-Remaining: 99
301
+ X-RateLimit-Reset: 1234567890
302
+ ```
303
+
304
+ When rate limited (429 Too Many Requests):
305
+
306
+ ```
307
+ Retry-After: 42
308
+ ```
309
+
310
+ ## Default 429 Response
311
+
312
+ ```json
313
+ {
314
+ "hitlimit": true,
315
+ "message": "Whoa there! Rate limit exceeded.",
316
+ "limit": 100,
317
+ "remaining": 0,
318
+ "resetIn": 42
319
+ }
320
+ ```
321
+
322
+ ## NestJS Decorators
323
+
324
+ Apply different limits to specific routes:
325
+
326
+ ```typescript
327
+ import { Controller, Get, UseGuards } from '@nestjs/common'
328
+ import { HitLimitGuard, HitLimit } from '@joint-ops/hitlimit/nest'
329
+
330
+ @Controller()
331
+ @UseGuards(HitLimitGuard)
332
+ export class AppController {
333
+ @Get()
334
+ @HitLimit({ limit: 10, window: '1m' })
335
+ strictEndpoint() {
336
+ return 'Limited to 10/min'
337
+ }
338
+
339
+ @Get('relaxed')
340
+ @HitLimit({ limit: 1000, window: '1m' })
341
+ relaxedEndpoint() {
342
+ return 'Limited to 1000/min'
343
+ }
344
+ }
345
+ ```
346
+
347
+ ## Related Packages
348
+
349
+ - [@joint-ops/hitlimit-bun](https://www.npmjs.com/package/@joint-ops/hitlimit-bun) - Bun-native rate limiting with bun:sqlite
350
+
351
+ ## Migrating from Other Libraries
352
+
353
+ ### From express-rate-limit
354
+
355
+ ```javascript
356
+ // Before (express-rate-limit)
357
+ import rateLimit from 'express-rate-limit'
358
+ app.use(rateLimit({ windowMs: 60000, max: 100 }))
359
+
360
+ // After (hitlimit) - 2x faster, smaller bundle
361
+ import { hitlimit } from '@joint-ops/hitlimit'
362
+ app.use(hitlimit({ window: '1m', limit: 100 }))
363
+ ```
364
+
365
+ ### From rate-limiter-flexible
366
+
367
+ ```javascript
368
+ // Before (rate-limiter-flexible)
369
+ import { RateLimiterMemory } from 'rate-limiter-flexible'
370
+ const limiter = new RateLimiterMemory({ points: 100, duration: 60 })
371
+
372
+ // After (hitlimit) - simpler API, better DX
373
+ import { hitlimit } from '@joint-ops/hitlimit'
374
+ app.use(hitlimit({ limit: 100, window: '1m' }))
375
+ ```
376
+
377
+ ### From @nestjs/throttler
378
+
379
+ ```typescript
380
+ // Before (@nestjs/throttler)
381
+ @UseGuards(ThrottlerGuard)
382
+ @Throttle({ default: { limit: 100, ttl: 60000 } })
383
+
384
+ // After (hitlimit) - more flexible, better performance
385
+ import { HitLimitGuard, HitLimit } from '@joint-ops/hitlimit/nest'
386
+ @UseGuards(HitLimitGuard)
387
+ @HitLimit({ limit: 100, window: '1m' })
388
+ ```
389
+
390
+ ## License
391
+
392
+ MIT - Use freely in personal and commercial projects.
393
+
394
+ ## Keywords
395
+
396
+ rate limit, rate limiter, rate limiting, express rate limit, express middleware, express-rate-limit, express-rate-limit alternative, nestjs rate limit, nestjs throttler, @nestjs/throttler alternative, nestjs guard, nodejs rate limit, node rate limiter, api rate limiting, throttle requests, request throttling, api throttling, ddos protection, brute force protection, redis rate limit, memory rate limit, sqlite rate limit, sliding window, fixed window, token bucket, leaky bucket, rate-limiter-flexible alternative, api security, request limiter, http rate limit, express slow down, api protection, login protection, authentication rate limit
@@ -0,0 +1,3 @@
1
+ import type { HitLimitOptions, HitLimitStore, KeyGenerator, ResolvedConfig } from '@joint-ops/hitlimit-types';
2
+ export declare function resolveConfig<TRequest>(options: HitLimitOptions<TRequest>, defaultStore: HitLimitStore, defaultKey: KeyGenerator<TRequest>): ResolvedConfig<TRequest>;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,cAAc,EACf,MAAM,2BAA2B,CAAA;AAGlC,wBAAgB,aAAa,CAAC,QAAQ,EACpC,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,EAClC,YAAY,EAAE,aAAa,EAC3B,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,GACjC,cAAc,CAAC,QAAQ,CAAC,CAiB1B"}
@@ -0,0 +1,20 @@
1
+ import { parseWindow } from './utils.js';
2
+ export function resolveConfig(options, defaultStore, defaultKey) {
3
+ return {
4
+ limit: options.limit ?? 100,
5
+ windowMs: parseWindow(options.window ?? '1m'),
6
+ key: options.key ?? defaultKey,
7
+ tiers: options.tiers,
8
+ tier: options.tier,
9
+ response: options.response ?? { hitlimit: true, message: 'Whoa there! Rate limit exceeded.' },
10
+ headers: {
11
+ standard: options.headers?.standard ?? true,
12
+ legacy: options.headers?.legacy ?? true,
13
+ retryAfter: options.headers?.retryAfter ?? true
14
+ },
15
+ store: options.store ?? defaultStore,
16
+ onStoreError: options.onStoreError ?? (() => 'allow'),
17
+ skip: options.skip
18
+ };
19
+ }
20
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAExC,MAAM,UAAU,aAAa,CAC3B,OAAkC,EAClC,YAA2B,EAC3B,UAAkC;IAElC,OAAO;QACL,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG;QAC3B,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC;QAC7C,GAAG,EAAE,OAAO,CAAC,GAAG,IAAI,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,kCAAkC,EAAE;QAC7F,OAAO,EAAE;YACP,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,QAAQ,IAAI,IAAI;YAC3C,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,MAAM,IAAI,IAAI;YACvC,UAAU,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,IAAI,IAAI;SAChD;QACD,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY;QACpC,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC;QACrD,IAAI,EAAE,OAAO,CAAC,IAAI;KACnB,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { HitLimitInfo, HeadersConfig } from '@joint-ops/hitlimit-types';
2
+ export declare function buildHeaders(info: HitLimitInfo, config: Required<HeadersConfig>, allowed: boolean): Record<string, string>;
3
+ //# sourceMappingURL=headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAE5E,wBAAgB,YAAY,CAC1B,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,EAC/B,OAAO,EAAE,OAAO,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoBxB"}
@@ -0,0 +1,18 @@
1
+ export function buildHeaders(info, config, allowed) {
2
+ const headers = {};
3
+ if (config.standard) {
4
+ headers['RateLimit-Limit'] = String(info.limit);
5
+ headers['RateLimit-Remaining'] = String(info.remaining);
6
+ headers['RateLimit-Reset'] = String(Math.ceil(info.resetAt / 1000));
7
+ }
8
+ if (config.legacy) {
9
+ headers['X-RateLimit-Limit'] = String(info.limit);
10
+ headers['X-RateLimit-Remaining'] = String(info.remaining);
11
+ headers['X-RateLimit-Reset'] = String(Math.ceil(info.resetAt / 1000));
12
+ }
13
+ if (!allowed && config.retryAfter) {
14
+ headers['Retry-After'] = String(info.resetIn);
15
+ }
16
+ return headers;
17
+ }
18
+ //# sourceMappingURL=headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/core/headers.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAC1B,IAAkB,EAClB,MAA+B,EAC/B,OAAgB;IAEhB,MAAM,OAAO,GAA2B,EAAE,CAAA;IAE1C,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;QACpB,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,OAAO,CAAC,qBAAqB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACvD,OAAO,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACrE,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,OAAO,CAAC,uBAAuB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACzD,OAAO,CAAC,mBAAmB,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAA;IACvE,CAAC;IAED,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;QAClC,OAAO,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/C,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { HitLimitResult, ResolvedConfig } from '@joint-ops/hitlimit-types';
2
+ export declare function checkLimit<TRequest>(config: ResolvedConfig<TRequest>, req: TRequest): Promise<HitLimitResult>;
3
+ //# sourceMappingURL=limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,cAAc,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAK7F,wBAAsB,UAAU,CAAC,QAAQ,EACvC,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,EAChC,GAAG,EAAE,QAAQ,GACZ,OAAO,CAAC,cAAc,CAAC,CAiDzB"}
@@ -0,0 +1,48 @@
1
+ import { parseWindow, hashKey } from './utils.js';
2
+ import { buildHeaders } from './headers.js';
3
+ import { buildBody } from './response.js';
4
+ export async function checkLimit(config, req) {
5
+ const rawKey = await config.key(req);
6
+ const key = hashKey(rawKey);
7
+ let limit = config.limit;
8
+ let windowMs = config.windowMs;
9
+ let tierName;
10
+ if (config.tier && config.tiers) {
11
+ tierName = await config.tier(req);
12
+ const tierConfig = config.tiers[tierName];
13
+ if (tierConfig) {
14
+ limit = tierConfig.limit;
15
+ if (tierConfig.window) {
16
+ windowMs = parseWindow(tierConfig.window);
17
+ }
18
+ }
19
+ }
20
+ if (limit === Infinity) {
21
+ return {
22
+ allowed: true,
23
+ info: { limit, remaining: Infinity, resetIn: 0, resetAt: 0, key: rawKey, tier: tierName },
24
+ headers: {},
25
+ body: {}
26
+ };
27
+ }
28
+ const result = await config.store.hit(key, windowMs, limit);
29
+ const now = Date.now();
30
+ const resetIn = Math.max(0, Math.ceil((result.resetAt - now) / 1000));
31
+ const remaining = Math.max(0, limit - result.count);
32
+ const allowed = result.count <= limit;
33
+ const info = {
34
+ limit,
35
+ remaining,
36
+ resetIn,
37
+ resetAt: result.resetAt,
38
+ key: rawKey,
39
+ tier: tierName
40
+ };
41
+ return {
42
+ allowed,
43
+ info,
44
+ headers: buildHeaders(info, config.headers, allowed),
45
+ body: allowed ? {} : buildBody(config.response, info)
46
+ };
47
+ }
48
+ //# sourceMappingURL=limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/core/limiter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAEzC,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAgC,EAChC,GAAa;IAEb,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3B,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;IACxB,IAAI,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;IAC9B,IAAI,QAA4B,CAAA;IAEhC,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAChC,QAAQ,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACjC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QACzC,IAAI,UAAU,EAAE,CAAC;YACf,KAAK,GAAG,UAAU,CAAC,KAAK,CAAA;YACxB,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC;gBACtB,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvB,OAAO;YACL,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;YACzF,OAAO,EAAE,EAAE;YACX,IAAI,EAAE,EAAE;SACT,CAAA;IACH,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC3D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;IACrE,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAA;IAErC,MAAM,IAAI,GAAiB;QACzB,KAAK;QACL,SAAS;QACT,OAAO;QACP,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,GAAG,EAAE,MAAM;QACX,IAAI,EAAE,QAAQ;KACf,CAAA;IAED,OAAO;QACL,OAAO;QACP,IAAI;QACJ,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;QACpD,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC;KACtD,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { HitLimitInfo, ResponseConfig, ResponseFormatter } from '@joint-ops/hitlimit-types';
2
+ export declare function buildBody(response: ResponseConfig | ResponseFormatter, info: HitLimitInfo): Record<string, any>;
3
+ //# sourceMappingURL=response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAEhG,wBAAgB,SAAS,CACvB,QAAQ,EAAE,cAAc,GAAG,iBAAiB,EAC5C,IAAI,EAAE,YAAY,GACjB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAWrB"}
@@ -0,0 +1,12 @@
1
+ export function buildBody(response, info) {
2
+ if (typeof response === 'function') {
3
+ return response(info);
4
+ }
5
+ return {
6
+ ...response,
7
+ limit: info.limit,
8
+ remaining: info.remaining,
9
+ resetIn: info.resetIn
10
+ };
11
+ }
12
+ //# sourceMappingURL=response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"response.js","sourceRoot":"","sources":["../../src/core/response.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CACvB,QAA4C,EAC5C,IAAkB;IAElB,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;QACnC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;IACvB,CAAC;IAED,OAAO;QACL,GAAG,QAAQ;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function parseWindow(window: string | number): number;
2
+ export declare function hashKey(key: string): string;
3
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AASA,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAO3D;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE3C"}
@@ -0,0 +1,19 @@
1
+ import { createHash } from 'crypto';
2
+ const UNITS = {
3
+ s: 1000,
4
+ m: 60 * 1000,
5
+ h: 60 * 60 * 1000,
6
+ d: 24 * 60 * 60 * 1000
7
+ };
8
+ export function parseWindow(window) {
9
+ if (typeof window === 'number')
10
+ return window;
11
+ const match = window.match(/^(\d+)(s|m|h|d)$/);
12
+ if (!match)
13
+ throw new Error(`Invalid window format: ${window}`);
14
+ return parseInt(match[1]) * UNITS[match[2]];
15
+ }
16
+ export function hashKey(key) {
17
+ return createHash('sha256').update(key).digest('hex').slice(0, 16);
18
+ }
19
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/core/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAEnC,MAAM,KAAK,GAA2B;IACpC,CAAC,EAAE,IAAI;IACP,CAAC,EAAE,EAAE,GAAG,IAAI;IACZ,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACjB,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;CACvB,CAAA;AAED,MAAM,UAAU,WAAW,CAAC,MAAuB;IACjD,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAA;IAE7C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;IAC9C,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,EAAE,CAAC,CAAA;IAE/D,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7C,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACpE,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import type { HitLimitOptions } from '@joint-ops/hitlimit-types';
3
+ export type { HitLimitOptions, HitLimitInfo, HitLimitResult, HitLimitStore, StoreResult, TierConfig, HeadersConfig, ResolvedConfig, KeyGenerator, TierResolver, SkipFunction, StoreErrorHandler, ResponseFormatter, ResponseConfig } from '@joint-ops/hitlimit-types';
4
+ export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
5
+ export { memoryStore } from './stores/memory.js';
6
+ export declare function hitlimit(options?: HitLimitOptions<Request>): (req: Request, res: Response, next: NextFunction) => Promise<void>;
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAKhE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AACrQ,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC7G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAMhD,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAe,CAAC,OAAO,CAAM,IAI/C,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBA8B9D"}
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { resolveConfig } from './core/config.js';
2
+ import { checkLimit } from './core/limiter.js';
3
+ import { memoryStore } from './stores/memory.js';
4
+ export { DEFAULT_LIMIT, DEFAULT_WINDOW, DEFAULT_WINDOW_MS, DEFAULT_MESSAGE } from '@joint-ops/hitlimit-types';
5
+ export { memoryStore } from './stores/memory.js';
6
+ function getDefaultKey(req) {
7
+ return req.ip || req.socket?.remoteAddress || 'unknown';
8
+ }
9
+ export function hitlimit(options = {}) {
10
+ const store = options.store ?? memoryStore();
11
+ const config = resolveConfig(options, store, getDefaultKey);
12
+ return async (req, res, next) => {
13
+ if (config.skip) {
14
+ const shouldSkip = await config.skip(req);
15
+ if (shouldSkip) {
16
+ return next();
17
+ }
18
+ }
19
+ try {
20
+ const result = await checkLimit(config, req);
21
+ Object.entries(result.headers).forEach(([key, value]) => {
22
+ res.setHeader(key, value);
23
+ });
24
+ if (!result.allowed) {
25
+ res.status(429).json(result.body);
26
+ return;
27
+ }
28
+ next();
29
+ }
30
+ catch (error) {
31
+ const action = await config.onStoreError(error, req);
32
+ if (action === 'deny') {
33
+ res.status(429).json({ hitlimit: true, message: 'Rate limit error' });
34
+ return;
35
+ }
36
+ next();
37
+ }
38
+ };
39
+ }
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC7G,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,SAAS,aAAa,CAAC,GAAY;IACjC,OAAO,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,EAAE,aAAa,IAAI,SAAS,CAAA;AACzD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,UAAoC,EAAE;IAC7D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,WAAW,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,EAAE,KAAK,EAAE,aAAa,CAAC,CAAA;IAE3D,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACzC,IAAI,UAAU,EAAE,CAAC;gBACf,OAAO,IAAI,EAAE,CAAA;YACf,CAAC;QACH,CAAC;QAED,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;YAE5C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;gBACtD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YAC3B,CAAC,CAAC,CAAA;YAEF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;gBACjC,OAAM;YACR,CAAC;YAED,IAAI,EAAE,CAAA;QACR,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,KAAc,EAAE,GAAG,CAAC,CAAA;YAC7D,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;gBACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC,CAAA;gBACrE,OAAM;YACR,CAAC;YACD,IAAI,EAAE,CAAA;QACR,CAAC;IACH,CAAC,CAAA;AACH,CAAC"}