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.
- package/README.md +438 -438
- package/package.json +2 -2
- package/src/analytics/tracker.js +107 -107
- package/src/core/config.js +127 -127
- package/src/core/limiter.js +229 -229
- package/src/index.js +108 -106
- package/src/smart/detector.js +117 -117
- package/src/store/memoryStore.js +122 -122
- package/src/store/redisStore.js +150 -0
- package/src/strategies/fixedWindow.js +40 -40
- package/src/strategies/slidingWindow.js +78 -78
- package/src/strategies/tokenBucket.js +85 -85
- package/src/utils/keyGenerator.js +95 -95
- package/src/utils/logger.js +76 -76
- package/types/index.d.ts +277 -246
|
@@ -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
|
|
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
|
-
`[
|
|
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 };
|
package/src/utils/logger.js
CHANGED
|
@@ -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. '[
|
|
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 };
|