nextlimiter 1.0.0 → 1.0.2

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.
@@ -1,85 +1,85 @@
1
- 'use strict';
2
-
3
- const { RateLimitResult } = require('../core/result');
4
-
5
- /**
6
- * Token Bucket strategy.
7
- *
8
- * Tokens refill continuously at a steady rate. Each request consumes one
9
- * token. Allows controlled bursts up to `capacity` tokens while enforcing
10
- * a long-term average rate of `max` requests per `windowMs`.
11
- *
12
- * State per key: { tokens: float, lastRefill: timestamp }
13
- *
14
- * Refill rate = config.max / config.windowMs (tokens per millisecond)
15
- * Capacity = config.max (maximum accumulated tokens)
16
- *
17
- * This is the algorithm used by Stripe for their API rate limiting.
18
- * It feels more "fair" to clients than window-based approaches because
19
- * occasional bursts are explicitly allowed.
20
- *
21
- * @param {string} key - Rate limit key
22
- * @param {object} config - Resolved NexLimit config
23
- * @param {object} store - Store instance
24
- * @returns {RateLimitResult}
25
- */
26
- function tokenBucketCheck(key, config, store) {
27
- const now = Date.now();
28
- const capacity = config.max;
29
- const refillRate = config.max / config.windowMs; // tokens per ms
30
- const storeKey = `${key}:tb`;
31
-
32
- let bucket = store.get(storeKey);
33
-
34
- if (!bucket) {
35
- // First request: full bucket, consume one token
36
- bucket = { tokens: capacity - 1, lastRefill: now };
37
- store.set(storeKey, bucket, config.windowMs * 2);
38
- return new RateLimitResult({
39
- allowed: true,
40
- limit: capacity,
41
- remaining: Math.floor(bucket.tokens),
42
- resetAt: now + config.windowMs,
43
- retryAfter: 0,
44
- key,
45
- strategy: 'token-bucket',
46
- });
47
- }
48
-
49
- // Refill tokens proportional to elapsed time
50
- const elapsed = now - bucket.lastRefill;
51
- bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * refillRate);
52
- bucket.lastRefill = now;
53
-
54
- if (bucket.tokens < 1) {
55
- // Not enough tokens: calculate when one token will be available
56
- const msUntilToken = Math.ceil((1 - bucket.tokens) / refillRate);
57
- store.set(storeKey, bucket, config.windowMs * 2);
58
-
59
- return new RateLimitResult({
60
- allowed: false,
61
- limit: capacity,
62
- remaining: 0,
63
- resetAt: now + msUntilToken,
64
- retryAfter: Math.ceil(msUntilToken / 1000),
65
- key,
66
- strategy: 'token-bucket',
67
- });
68
- }
69
-
70
- // Consume one token
71
- bucket.tokens -= 1;
72
- store.set(storeKey, bucket, config.windowMs * 2);
73
-
74
- return new RateLimitResult({
75
- allowed: true,
76
- limit: capacity,
77
- remaining: Math.floor(bucket.tokens),
78
- resetAt: now + Math.ceil((capacity - bucket.tokens) / refillRate),
79
- retryAfter: 0,
80
- key,
81
- strategy: 'token-bucket',
82
- });
83
- }
84
-
85
- module.exports = { tokenBucketCheck };
1
+ 'use strict';
2
+
3
+ const { RateLimitResult } = require('../core/result');
4
+
5
+ /**
6
+ * Token Bucket strategy.
7
+ *
8
+ * Tokens refill continuously at a steady rate. Each request consumes one
9
+ * token. Allows controlled bursts up to `capacity` tokens while enforcing
10
+ * a long-term average rate of `max` requests per `windowMs`.
11
+ *
12
+ * State per key: { tokens: float, lastRefill: timestamp }
13
+ *
14
+ * Refill rate = config.max / config.windowMs (tokens per millisecond)
15
+ * Capacity = config.max (maximum accumulated tokens)
16
+ *
17
+ * This is the algorithm used by Stripe for their API rate limiting.
18
+ * It feels more "fair" to clients than window-based approaches because
19
+ * occasional bursts are explicitly allowed.
20
+ *
21
+ * @param {string} key - Rate limit key
22
+ * @param {object} config - Resolved NextLimiter config
23
+ * @param {object} store - Store instance
24
+ * @returns {RateLimitResult}
25
+ */
26
+ function tokenBucketCheck(key, config, store) {
27
+ const now = Date.now();
28
+ const capacity = config.max;
29
+ const refillRate = config.max / config.windowMs; // tokens per ms
30
+ const storeKey = `${key}:tb`;
31
+
32
+ let bucket = store.get(storeKey);
33
+
34
+ if (!bucket) {
35
+ // First request: full bucket, consume one token
36
+ bucket = { tokens: capacity - 1, lastRefill: now };
37
+ store.set(storeKey, bucket, config.windowMs * 2);
38
+ return new RateLimitResult({
39
+ allowed: true,
40
+ limit: capacity,
41
+ remaining: Math.floor(bucket.tokens),
42
+ resetAt: now + config.windowMs,
43
+ retryAfter: 0,
44
+ key,
45
+ strategy: 'token-bucket',
46
+ });
47
+ }
48
+
49
+ // Refill tokens proportional to elapsed time
50
+ const elapsed = now - bucket.lastRefill;
51
+ bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * refillRate);
52
+ bucket.lastRefill = now;
53
+
54
+ if (bucket.tokens < 1) {
55
+ // Not enough tokens: calculate when one token will be available
56
+ const msUntilToken = Math.ceil((1 - bucket.tokens) / refillRate);
57
+ store.set(storeKey, bucket, config.windowMs * 2);
58
+
59
+ return new RateLimitResult({
60
+ allowed: false,
61
+ limit: capacity,
62
+ remaining: 0,
63
+ resetAt: now + msUntilToken,
64
+ retryAfter: Math.ceil(msUntilToken / 1000),
65
+ key,
66
+ strategy: 'token-bucket',
67
+ });
68
+ }
69
+
70
+ // Consume one token
71
+ bucket.tokens -= 1;
72
+ store.set(storeKey, bucket, config.windowMs * 2);
73
+
74
+ return new RateLimitResult({
75
+ allowed: true,
76
+ limit: capacity,
77
+ remaining: Math.floor(bucket.tokens),
78
+ resetAt: now + Math.ceil((capacity - bucket.tokens) / refillRate),
79
+ retryAfter: 0,
80
+ key,
81
+ strategy: 'token-bucket',
82
+ });
83
+ }
84
+
85
+ module.exports = { tokenBucketCheck };
@@ -1,95 +1,95 @@
1
- 'use strict';
2
-
3
- /**
4
- * Built-in key generators.
5
- * Each returns a string that uniquely identifies the rate-limit subject.
6
- */
7
-
8
- /**
9
- * Extract real client IP, respecting common proxy headers.
10
- * Order: X-Forwarded-For → X-Real-IP → req.ip → req.socket.remoteAddress
11
- *
12
- * @param {object} req - Express request
13
- * @returns {string}
14
- */
15
- function getIP(req) {
16
- const forwarded = req.headers['x-forwarded-for'];
17
- if (forwarded) {
18
- // X-Forwarded-For can be a comma-separated list; first entry is client IP
19
- return forwarded.split(',')[0].trim();
20
- }
21
- return (
22
- req.headers['x-real-ip'] ||
23
- req.ip ||
24
- (req.socket && req.socket.remoteAddress) ||
25
- 'unknown'
26
- );
27
- }
28
-
29
- /**
30
- * Rate limit by authenticated user ID.
31
- * Looks for userId in: req.user.id → req.user._id → req.userId → req.auth.userId
32
- *
33
- * Falls back to IP if no user is found (unauthenticated requests).
34
- *
35
- * @param {object} req
36
- * @returns {string}
37
- */
38
- function getUserId(req) {
39
- const userId =
40
- (req.user && (req.user.id || req.user._id || req.user.userId)) ||
41
- req.userId ||
42
- (req.auth && req.auth.userId);
43
-
44
- return userId ? `user:${userId}` : `ip:${getIP(req)}`;
45
- }
46
-
47
- /**
48
- * Rate limit by API key.
49
- * Looks for key in: Authorization header (Bearer) → X-API-Key header → query.apiKey
50
- *
51
- * Falls back to IP if no API key found.
52
- *
53
- * @param {object} req
54
- * @returns {string}
55
- */
56
- function getApiKey(req) {
57
- // Bearer token in Authorization header
58
- const auth = req.headers['authorization'];
59
- if (auth && auth.startsWith('Bearer ')) {
60
- return `apikey:${auth.slice(7)}`;
61
- }
62
-
63
- // X-API-Key custom header
64
- const headerKey = req.headers['x-api-key'];
65
- if (headerKey) return `apikey:${headerKey}`;
66
-
67
- // Query parameter fallback
68
- const queryKey = req.query && req.query.apiKey;
69
- if (queryKey) return `apikey:${queryKey}`;
70
-
71
- // Final fallback: IP
72
- return `ip:${getIP(req)}`;
73
- }
74
-
75
- /**
76
- * Resolve a key generator function from the `keyBy` config value.
77
- *
78
- * @param {string|function} keyBy - 'ip' | 'user-id' | 'api-key' | custom fn
79
- * @returns {function}
80
- */
81
- function resolveKeyGenerator(keyBy) {
82
- if (typeof keyBy === 'function') return keyBy;
83
-
84
- switch (keyBy) {
85
- case 'ip': return getIP;
86
- case 'user-id': return getUserId;
87
- case 'api-key': return getApiKey;
88
- default:
89
- throw new Error(
90
- `[NexLimit] Unknown keyBy value: "${keyBy}". Use 'ip', 'user-id', 'api-key', or a custom function.`
91
- );
92
- }
93
- }
94
-
95
- module.exports = { getIP, getUserId, getApiKey, resolveKeyGenerator };
1
+ 'use strict';
2
+
3
+ /**
4
+ * Built-in key generators.
5
+ * Each returns a string that uniquely identifies the rate-limit subject.
6
+ */
7
+
8
+ /**
9
+ * Extract real client IP, respecting common proxy headers.
10
+ * Order: X-Forwarded-For → X-Real-IP → req.ip → req.socket.remoteAddress
11
+ *
12
+ * @param {object} req - Express request
13
+ * @returns {string}
14
+ */
15
+ function getIP(req) {
16
+ const forwarded = req.headers['x-forwarded-for'];
17
+ if (forwarded) {
18
+ // X-Forwarded-For can be a comma-separated list; first entry is client IP
19
+ return forwarded.split(',')[0].trim();
20
+ }
21
+ return (
22
+ req.headers['x-real-ip'] ||
23
+ req.ip ||
24
+ (req.socket && req.socket.remoteAddress) ||
25
+ 'unknown'
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Rate limit by authenticated user ID.
31
+ * Looks for userId in: req.user.id → req.user._id → req.userId → req.auth.userId
32
+ *
33
+ * Falls back to IP if no user is found (unauthenticated requests).
34
+ *
35
+ * @param {object} req
36
+ * @returns {string}
37
+ */
38
+ function getUserId(req) {
39
+ const userId =
40
+ (req.user && (req.user.id || req.user._id || req.user.userId)) ||
41
+ req.userId ||
42
+ (req.auth && req.auth.userId);
43
+
44
+ return userId ? `user:${userId}` : `ip:${getIP(req)}`;
45
+ }
46
+
47
+ /**
48
+ * Rate limit by API key.
49
+ * Looks for key in: Authorization header (Bearer) → X-API-Key header → query.apiKey
50
+ *
51
+ * Falls back to IP if no API key found.
52
+ *
53
+ * @param {object} req
54
+ * @returns {string}
55
+ */
56
+ function getApiKey(req) {
57
+ // Bearer token in Authorization header
58
+ const auth = req.headers['authorization'];
59
+ if (auth && auth.startsWith('Bearer ')) {
60
+ return `apikey:${auth.slice(7)}`;
61
+ }
62
+
63
+ // X-API-Key custom header
64
+ const headerKey = req.headers['x-api-key'];
65
+ if (headerKey) return `apikey:${headerKey}`;
66
+
67
+ // Query parameter fallback
68
+ const queryKey = req.query && req.query.apiKey;
69
+ if (queryKey) return `apikey:${queryKey}`;
70
+
71
+ // Final fallback: IP
72
+ return `ip:${getIP(req)}`;
73
+ }
74
+
75
+ /**
76
+ * Resolve a key generator function from the `keyBy` config value.
77
+ *
78
+ * @param {string|function} keyBy - 'ip' | 'user-id' | 'api-key' | custom fn
79
+ * @returns {function}
80
+ */
81
+ function resolveKeyGenerator(keyBy) {
82
+ if (typeof keyBy === 'function') return keyBy;
83
+
84
+ switch (keyBy) {
85
+ case 'ip': return getIP;
86
+ case 'user-id': return getUserId;
87
+ case 'api-key': return getApiKey;
88
+ default:
89
+ throw new Error(
90
+ `[NextLimiter] Unknown keyBy value: "${keyBy}". Use 'ip', 'user-id', 'api-key', or a custom function.`
91
+ );
92
+ }
93
+ }
94
+
95
+ module.exports = { getIP, getUserId, getApiKey, resolveKeyGenerator };
@@ -1,76 +1,76 @@
1
- 'use strict';
2
-
3
- // ANSI color codes — auto-disabled on non-TTY environments
4
- const isTTY = process.stdout && process.stdout.isTTY;
5
- const c = {
6
- reset: isTTY ? '\x1b[0m' : '',
7
- bold: isTTY ? '\x1b[1m' : '',
8
- dim: isTTY ? '\x1b[2m' : '',
9
- red: isTTY ? '\x1b[31m' : '',
10
- green: isTTY ? '\x1b[32m' : '',
11
- yellow: isTTY ? '\x1b[33m' : '',
12
- cyan: isTTY ? '\x1b[36m' : '',
13
- gray: isTTY ? '\x1b[90m' : '',
14
- };
15
-
16
- /**
17
- * Create a logger bound to a specific prefix and enabled flag.
18
- *
19
- * @param {string} prefix - e.g. '[NexLimit]'
20
- * @param {boolean} enabled - logging on/off
21
- * @returns {{ blocked, allowed, warn, info }}
22
- */
23
- function createLogger(prefix, enabled) {
24
- if (!enabled) {
25
- return {
26
- blocked: () => {},
27
- allowed: () => {},
28
- warn: () => {},
29
- info: () => {},
30
- };
31
- }
32
-
33
- const tag = `${c.bold}${c.cyan}${prefix}${c.reset}`;
34
- const ts = () => `${c.gray}${new Date().toISOString()}${c.reset}`;
35
-
36
- return {
37
- /**
38
- * Log a blocked request.
39
- * @param {string} key
40
- * @param {number} count
41
- * @param {number} limit
42
- * @param {string} strategy
43
- * @param {boolean} smart
44
- */
45
- blocked(key, count, limit, strategy, smart = false) {
46
- const smartTag = smart ? ` ${c.yellow}[smart]${c.reset}` : '';
47
- console.log(
48
- `${ts()} ${tag} ${c.red}BLOCKED${c.reset}${smartTag} ` +
49
- `${c.bold}${key}${c.reset} ` +
50
- `${c.red}(${count}/${limit})${c.reset} ` +
51
- `${c.dim}via ${strategy}${c.reset}`
52
- );
53
- },
54
-
55
- /**
56
- * Log an allowed request (only useful for debugging — off by default).
57
- */
58
- allowed(key, remaining, limit) {
59
- console.log(
60
- `${ts()} ${tag} ${c.green}ALLOWED${c.reset} ` +
61
- `${c.bold}${key}${c.reset} ` +
62
- `${c.dim}(${remaining}/${limit} remaining)${c.reset}`
63
- );
64
- },
65
-
66
- warn(message) {
67
- console.warn(`${ts()} ${tag} ${c.yellow}WARN${c.reset} ${message}`);
68
- },
69
-
70
- info(message) {
71
- console.log(`${ts()} ${tag} ${c.cyan}INFO${c.reset} ${message}`);
72
- },
73
- };
74
- }
75
-
76
- module.exports = { createLogger };
1
+ 'use strict';
2
+
3
+ // ANSI color codes — auto-disabled on non-TTY environments
4
+ const isTTY = process.stdout && process.stdout.isTTY;
5
+ const c = {
6
+ reset: isTTY ? '\x1b[0m' : '',
7
+ bold: isTTY ? '\x1b[1m' : '',
8
+ dim: isTTY ? '\x1b[2m' : '',
9
+ red: isTTY ? '\x1b[31m' : '',
10
+ green: isTTY ? '\x1b[32m' : '',
11
+ yellow: isTTY ? '\x1b[33m' : '',
12
+ cyan: isTTY ? '\x1b[36m' : '',
13
+ gray: isTTY ? '\x1b[90m' : '',
14
+ };
15
+
16
+ /**
17
+ * Create a logger bound to a specific prefix and enabled flag.
18
+ *
19
+ * @param {string} prefix - e.g. '[NextLimiter]'
20
+ * @param {boolean} enabled - logging on/off
21
+ * @returns {{ blocked, allowed, warn, info }}
22
+ */
23
+ function createLogger(prefix, enabled) {
24
+ if (!enabled) {
25
+ return {
26
+ blocked: () => {},
27
+ allowed: () => {},
28
+ warn: () => {},
29
+ info: () => {},
30
+ };
31
+ }
32
+
33
+ const tag = `${c.bold}${c.cyan}${prefix}${c.reset}`;
34
+ const ts = () => `${c.gray}${new Date().toISOString()}${c.reset}`;
35
+
36
+ return {
37
+ /**
38
+ * Log a blocked request.
39
+ * @param {string} key
40
+ * @param {number} count
41
+ * @param {number} limit
42
+ * @param {string} strategy
43
+ * @param {boolean} smart
44
+ */
45
+ blocked(key, count, limit, strategy, smart = false) {
46
+ const smartTag = smart ? ` ${c.yellow}[smart]${c.reset}` : '';
47
+ console.log(
48
+ `${ts()} ${tag} ${c.red}BLOCKED${c.reset}${smartTag} ` +
49
+ `${c.bold}${key}${c.reset} ` +
50
+ `${c.red}(${count}/${limit})${c.reset} ` +
51
+ `${c.dim}via ${strategy}${c.reset}`
52
+ );
53
+ },
54
+
55
+ /**
56
+ * Log an allowed request (only useful for debugging — off by default).
57
+ */
58
+ allowed(key, remaining, limit) {
59
+ console.log(
60
+ `${ts()} ${tag} ${c.green}ALLOWED${c.reset} ` +
61
+ `${c.bold}${key}${c.reset} ` +
62
+ `${c.dim}(${remaining}/${limit} remaining)${c.reset}`
63
+ );
64
+ },
65
+
66
+ warn(message) {
67
+ console.warn(`${ts()} ${tag} ${c.yellow}WARN${c.reset} ${message}`);
68
+ },
69
+
70
+ info(message) {
71
+ console.log(`${ts()} ${tag} ${c.cyan}INFO${c.reset} ${message}`);
72
+ },
73
+ };
74
+ }
75
+
76
+ module.exports = { createLogger };