nextlimiter 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.
- package/README.md +438 -0
- package/package.json +70 -0
- package/src/analytics/tracker.js +107 -0
- package/src/core/config.js +127 -0
- package/src/core/limiter.js +229 -0
- package/src/core/result.js +49 -0
- package/src/index.js +106 -0
- package/src/middleware/headers.js +29 -0
- package/src/smart/detector.js +117 -0
- package/src/store/memoryStore.js +122 -0
- package/src/strategies/fixedWindow.js +40 -0
- package/src/strategies/slidingWindow.js +78 -0
- package/src/strategies/tokenBucket.js +85 -0
- package/src/utils/keyGenerator.js +95 -0
- package/src/utils/logger.js +76 -0
- package/types/index.d.ts +246 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MemoryStore — default storage backend.
|
|
5
|
+
*
|
|
6
|
+
* Uses a plain Map with periodic cleanup of expired entries.
|
|
7
|
+
* For multi-process / distributed deployments, swap this for RedisStore.
|
|
8
|
+
*
|
|
9
|
+
* Implements the NexLimit Store interface:
|
|
10
|
+
* get(key) → value | undefined
|
|
11
|
+
* set(key, value, ttlMs)
|
|
12
|
+
* increment(key, ttlMs) → number (atomic increment, returns new value)
|
|
13
|
+
* delete(key)
|
|
14
|
+
* keys() → string[]
|
|
15
|
+
* clear()
|
|
16
|
+
*/
|
|
17
|
+
class MemoryStore {
|
|
18
|
+
constructor() {
|
|
19
|
+
/** @type {Map<string, { value: any, expiresAt: number }>} */
|
|
20
|
+
this._data = new Map();
|
|
21
|
+
|
|
22
|
+
// Clean up expired entries every 5 minutes to prevent memory leaks
|
|
23
|
+
this._cleanupInterval = setInterval(() => this._cleanup(), 5 * 60_000);
|
|
24
|
+
|
|
25
|
+
// Don't let this timer prevent process exit
|
|
26
|
+
if (this._cleanupInterval.unref) this._cleanupInterval.unref();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a stored value by key. Returns undefined if missing or expired.
|
|
31
|
+
* @param {string} key
|
|
32
|
+
*/
|
|
33
|
+
get(key) {
|
|
34
|
+
const entry = this._data.get(key);
|
|
35
|
+
if (!entry) return undefined;
|
|
36
|
+
if (Date.now() > entry.expiresAt) {
|
|
37
|
+
this._data.delete(key);
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return entry.value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Set a value with TTL.
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @param {any} value
|
|
47
|
+
* @param {number} ttlMs - Time to live in milliseconds
|
|
48
|
+
*/
|
|
49
|
+
set(key, value, ttlMs) {
|
|
50
|
+
this._data.set(key, {
|
|
51
|
+
value,
|
|
52
|
+
expiresAt: Date.now() + ttlMs,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Atomic increment. Creates key with value 1 if it doesn't exist.
|
|
58
|
+
* Returns the new value after increment.
|
|
59
|
+
* @param {string} key
|
|
60
|
+
* @param {number} ttlMs - TTL applied only on first creation
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
increment(key, ttlMs) {
|
|
64
|
+
const entry = this._data.get(key);
|
|
65
|
+
const now = Date.now();
|
|
66
|
+
|
|
67
|
+
if (!entry || now > entry.expiresAt) {
|
|
68
|
+
this._data.set(key, { value: 1, expiresAt: now + ttlMs });
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
entry.value += 1;
|
|
73
|
+
return entry.value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Delete a key immediately.
|
|
78
|
+
* @param {string} key
|
|
79
|
+
*/
|
|
80
|
+
delete(key) {
|
|
81
|
+
this._data.delete(key);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Return all non-expired keys.
|
|
86
|
+
* @returns {string[]}
|
|
87
|
+
*/
|
|
88
|
+
keys() {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const result = [];
|
|
91
|
+
for (const [key, entry] of this._data) {
|
|
92
|
+
if (now <= entry.expiresAt) result.push(key);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Remove all entries. */
|
|
98
|
+
clear() {
|
|
99
|
+
this._data.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Remove expired entries to prevent unbounded memory growth. */
|
|
103
|
+
_cleanup() {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
for (const [key, entry] of this._data) {
|
|
106
|
+
if (now > entry.expiresAt) this._data.delete(key);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Stop the cleanup timer. Call when tearing down the store. */
|
|
111
|
+
destroy() {
|
|
112
|
+
clearInterval(this._cleanupInterval);
|
|
113
|
+
this._data.clear();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Current number of tracked keys (includes expired until next cleanup). */
|
|
117
|
+
get size() {
|
|
118
|
+
return this._data.size;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { MemoryStore };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { RateLimitResult } = require('../core/result');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fixed Window strategy.
|
|
7
|
+
*
|
|
8
|
+
* Divides time into fixed intervals. Counts requests per interval.
|
|
9
|
+
* Simple, low memory usage, but susceptible to boundary burst attacks
|
|
10
|
+
* (a client can send 2× the limit in a short period by straddling a window edge).
|
|
11
|
+
*
|
|
12
|
+
* Redis key format: `<prefix><key>:<windowStart>`
|
|
13
|
+
* The window timestamp in the key means entries auto-expire naturally.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} key - Rate limit key (e.g. "nexlimit:ip:1.2.3.4")
|
|
16
|
+
* @param {object} config - Resolved NexLimit config
|
|
17
|
+
* @param {object} store - Store instance
|
|
18
|
+
* @returns {RateLimitResult}
|
|
19
|
+
*/
|
|
20
|
+
function fixedWindowCheck(key, config, store) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const windowStart = Math.floor(now / config.windowMs) * config.windowMs;
|
|
23
|
+
const windowEnd = windowStart + config.windowMs;
|
|
24
|
+
const storeKey = `${key}:fw:${windowStart}`;
|
|
25
|
+
|
|
26
|
+
const count = store.increment(storeKey, config.windowMs);
|
|
27
|
+
const allowed = count <= config.max;
|
|
28
|
+
|
|
29
|
+
return new RateLimitResult({
|
|
30
|
+
allowed,
|
|
31
|
+
limit: config.max,
|
|
32
|
+
remaining: config.max - count,
|
|
33
|
+
resetAt: windowEnd,
|
|
34
|
+
retryAfter: allowed ? 0 : Math.ceil((windowEnd - now) / 1000),
|
|
35
|
+
key,
|
|
36
|
+
strategy: 'fixed-window',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { fixedWindowCheck };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { RateLimitResult } = require('../core/result');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sliding Window strategy using a weighted two-window approximation.
|
|
7
|
+
*
|
|
8
|
+
* This is the algorithm used by Cloudflare and Nginx's limit_req_zone.
|
|
9
|
+
* It avoids the boundary-burst problem of fixed window without the memory
|
|
10
|
+
* cost of a full sliding-window log.
|
|
11
|
+
*
|
|
12
|
+
* How it works:
|
|
13
|
+
* count ≈ (prevWindowCount × prevWindowWeight) + currentWindowCount
|
|
14
|
+
*
|
|
15
|
+
* Where prevWindowWeight = fraction of previous window still "in view"
|
|
16
|
+
* based on how far into the current window we are.
|
|
17
|
+
*
|
|
18
|
+
* Example: windowMs = 60s, we are 15s into the current window.
|
|
19
|
+
* prevWindowWeight = (60-15)/60 = 0.75
|
|
20
|
+
* Effective count = (prevCount × 0.75) + currentCount
|
|
21
|
+
*
|
|
22
|
+
* This is O(1) memory per key — same as fixed window — but much more
|
|
23
|
+
* accurate. The approximation error is at most 1/(2 × windowMs).
|
|
24
|
+
*
|
|
25
|
+
* @param {string} key - Rate limit key
|
|
26
|
+
* @param {object} config - Resolved NexLimit config
|
|
27
|
+
* @param {object} store - Store instance
|
|
28
|
+
* @returns {RateLimitResult}
|
|
29
|
+
*/
|
|
30
|
+
function slidingWindowCheck(key, config, store) {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const windowMs = config.windowMs;
|
|
33
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
34
|
+
const prevStart = windowStart - windowMs;
|
|
35
|
+
|
|
36
|
+
const currentKey = `${key}:sw:${windowStart}`;
|
|
37
|
+
const prevKey = `${key}:sw:${prevStart}`;
|
|
38
|
+
|
|
39
|
+
// How far into the current window are we? (0.0 → 1.0)
|
|
40
|
+
const positionInWindow = (now - windowStart) / windowMs;
|
|
41
|
+
|
|
42
|
+
// Weight of the previous window (complement of current position)
|
|
43
|
+
const prevWeight = 1 - positionInWindow;
|
|
44
|
+
|
|
45
|
+
// Get counts from both windows
|
|
46
|
+
const currentCount = store.get(currentKey) || 0;
|
|
47
|
+
const prevCount = store.get(prevKey) || 0;
|
|
48
|
+
|
|
49
|
+
// Weighted approximation
|
|
50
|
+
const effectiveCount = Math.floor(prevCount * prevWeight) + currentCount;
|
|
51
|
+
|
|
52
|
+
if (effectiveCount >= config.max) {
|
|
53
|
+
return new RateLimitResult({
|
|
54
|
+
allowed: false,
|
|
55
|
+
limit: config.max,
|
|
56
|
+
remaining: 0,
|
|
57
|
+
resetAt: windowStart + windowMs,
|
|
58
|
+
retryAfter: Math.ceil((windowStart + windowMs - now) / 1000),
|
|
59
|
+
key,
|
|
60
|
+
strategy: 'sliding-window',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Increment current window. TTL = 2 full windows to keep prev window alive.
|
|
65
|
+
store.increment(currentKey, windowMs * 2);
|
|
66
|
+
|
|
67
|
+
return new RateLimitResult({
|
|
68
|
+
allowed: true,
|
|
69
|
+
limit: config.max,
|
|
70
|
+
remaining: config.max - effectiveCount - 1,
|
|
71
|
+
resetAt: windowStart + windowMs,
|
|
72
|
+
retryAfter: 0,
|
|
73
|
+
key,
|
|
74
|
+
strategy: 'sliding-window',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { slidingWindowCheck };
|
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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 };
|