nextlimiter 1.0.5 → 1.1.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/package.json +1 -1
- package/src/analytics/prometheus.js +122 -0
- package/src/core/accessControl.js +3 -1
- package/src/core/config.js +12 -0
- package/src/core/limiter.js +85 -6
- package/src/index.js +4 -0
- package/src/smart/detector.js +17 -1
- package/types/index.d.ts +56 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextlimiter",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Production-ready rate limiting for Node.js — sliding window, token bucket, SaaS plans, smart limiting, and built-in analytics.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure Node.js Prometheus Exposition Formatter
|
|
5
|
+
* Zero dependencies. Converts limiter stats to valid Prometheus text format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class PrometheusFormatter {
|
|
9
|
+
constructor(limiter) {
|
|
10
|
+
this.limiter = limiter;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
contentType() {
|
|
14
|
+
return 'text/plain; version=0.0.4; charset=utf-8';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape label values according to Prometheus rules:
|
|
19
|
+
* \ -> \\
|
|
20
|
+
* " -> \"
|
|
21
|
+
* \n -> \n
|
|
22
|
+
*/
|
|
23
|
+
_escapeLabel(str) {
|
|
24
|
+
return String(str)
|
|
25
|
+
.replace(/\\/g, '\\\\')
|
|
26
|
+
.replace(/"/g, '\\"')
|
|
27
|
+
.replace(/\n/g, '\\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format a single metric block (help, type, and samples).
|
|
32
|
+
*/
|
|
33
|
+
_metricBlock(name, help, type, samples) {
|
|
34
|
+
if (!samples || samples.length === 0) return '';
|
|
35
|
+
let block = `# HELP ${name} ${help}\n`;
|
|
36
|
+
block += `# TYPE ${name} ${type}\n`;
|
|
37
|
+
|
|
38
|
+
for (const sample of samples) {
|
|
39
|
+
const labelEntries = Object.entries(sample.labels || {});
|
|
40
|
+
if (labelEntries.length > 0) {
|
|
41
|
+
const labelStrs = labelEntries.map(([k, v]) => `${k}="${this._escapeLabel(v)}"`);
|
|
42
|
+
block += `${name}{${labelStrs.join(',')}} ${sample.value}\n`;
|
|
43
|
+
} else {
|
|
44
|
+
block += `${name} ${sample.value}\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return block + '\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
format() {
|
|
51
|
+
const stats = this.limiter.getStats();
|
|
52
|
+
let output = '';
|
|
53
|
+
|
|
54
|
+
// 1. Total requests (Counter)
|
|
55
|
+
output += this._metricBlock(
|
|
56
|
+
'nextlimiter_requests_total',
|
|
57
|
+
'Total requests processed by nextlimiter',
|
|
58
|
+
'counter',
|
|
59
|
+
[
|
|
60
|
+
{ labels: { status: 'allowed' }, value: stats.allowedRequests },
|
|
61
|
+
{ labels: { status: 'blocked' }, value: stats.blockedRequests }
|
|
62
|
+
]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 2. Block rate (Gauge)
|
|
66
|
+
output += this._metricBlock(
|
|
67
|
+
'nextlimiter_block_rate',
|
|
68
|
+
'Ratio of blocked to total requests (0.0 to 1.0)',
|
|
69
|
+
'gauge',
|
|
70
|
+
[{ labels: {}, value: isNaN(stats.blockRate) ? 0 : stats.blockRate }]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 3. Top blocked IPs (Gauge)
|
|
74
|
+
const topBlockedSamples = (stats.topBlocked || []).slice(0, 10).map((item) => {
|
|
75
|
+
// Clean up IP label
|
|
76
|
+
let ip = item.key.replace(/^nextlimiter:(ip:)?/, '');
|
|
77
|
+
return { labels: { ip }, value: item.count };
|
|
78
|
+
});
|
|
79
|
+
if (topBlockedSamples.length > 0) {
|
|
80
|
+
output += this._metricBlock(
|
|
81
|
+
'nextlimiter_top_blocked_ip',
|
|
82
|
+
'Block count per offending IP',
|
|
83
|
+
'gauge',
|
|
84
|
+
topBlockedSamples
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Top keys by volume (Gauge)
|
|
89
|
+
const topKeysSamples = (stats.topKeys || []).slice(0, 10).map((item) => {
|
|
90
|
+
return { labels: { key: item.key }, value: item.count };
|
|
91
|
+
});
|
|
92
|
+
if (topKeysSamples.length > 0) {
|
|
93
|
+
output += this._metricBlock(
|
|
94
|
+
'nextlimiter_top_keys_total',
|
|
95
|
+
'Total request count per key',
|
|
96
|
+
'gauge',
|
|
97
|
+
topKeysSamples
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 5. Uptime seconds (Gauge)
|
|
102
|
+
output += this._metricBlock(
|
|
103
|
+
'nextlimiter_uptime_seconds',
|
|
104
|
+
'Seconds since limiter was created',
|
|
105
|
+
'gauge',
|
|
106
|
+
[{ labels: {}, value: stats.uptimeMs / 1000 }]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 6. Info (Gauge = 1)
|
|
110
|
+
const version = '1.0.5'; // Current pkg version
|
|
111
|
+
output += this._metricBlock(
|
|
112
|
+
'nextlimiter_info',
|
|
113
|
+
'Static info about the limiter instance',
|
|
114
|
+
'gauge',
|
|
115
|
+
[{ labels: { strategy: stats.config.strategy, version }, value: 1 }]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { PrometheusFormatter };
|
|
@@ -21,14 +21,16 @@ const { ipMatchesList } = require('../utils/cidr');
|
|
|
21
21
|
* checkAccess('1.2.3.4', {})
|
|
22
22
|
* // → { action: 'allow', reason: 'proceed' }
|
|
23
23
|
*/
|
|
24
|
-
function checkAccess(ip, config) {
|
|
24
|
+
function checkAccess(ip, config, emitter) {
|
|
25
25
|
// Blacklist wins — always checked first regardless of whitelist
|
|
26
26
|
if (config.blacklist && ipMatchesList(ip, config.blacklist)) {
|
|
27
|
+
if (emitter) emitter.emit('blacklisted', ip);
|
|
27
28
|
return { action: 'block', reason: 'blacklisted' };
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// Whitelist — bypass all rate limiting
|
|
31
32
|
if (config.whitelist && ipMatchesList(ip, config.whitelist)) {
|
|
33
|
+
if (emitter) emitter.emit('whitelisted', ip);
|
|
32
34
|
return { action: 'skip', reason: 'whitelisted' };
|
|
33
35
|
}
|
|
34
36
|
|
package/src/core/config.js
CHANGED
|
@@ -87,6 +87,7 @@ const DEFAULT_CONFIG = {
|
|
|
87
87
|
keyGenerator: null, // (req) => string — custom key fn
|
|
88
88
|
whitelist: null, // string[] — IPs/CIDRs that bypass rate limiting
|
|
89
89
|
blacklist: null, // string[] — IPs/CIDRs that always get 403
|
|
90
|
+
statsInterval: undefined, // ms interval to emit 'stats' event
|
|
90
91
|
};
|
|
91
92
|
|
|
92
93
|
/**
|
|
@@ -147,6 +148,17 @@ function resolveConfig(userOptions = {}) {
|
|
|
147
148
|
base[listName] = valid.length > 0 ? valid : null;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
// Validate statsInterval
|
|
152
|
+
if (base.statsInterval !== undefined) {
|
|
153
|
+
if (typeof base.statsInterval !== 'number' || base.statsInterval <= 0) {
|
|
154
|
+
console.warn('[NextLimiter] config.statsInterval must be a positive number. Disabling.');
|
|
155
|
+
base.statsInterval = undefined;
|
|
156
|
+
} else if (base.statsInterval < 1000) {
|
|
157
|
+
console.warn('[NextLimiter] config.statsInterval must be at least 1000ms. Clamping to 1000ms.');
|
|
158
|
+
base.statsInterval = 1000;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
150
162
|
return base;
|
|
151
163
|
}
|
|
152
164
|
|
package/src/core/limiter.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const EventEmitter = require('events');
|
|
3
4
|
const { resolveConfig } = require('./config');
|
|
4
5
|
const { MemoryStore } = require('../store/memoryStore');
|
|
5
6
|
const { fixedWindowCheck } = require('../strategies/fixedWindow');
|
|
@@ -12,6 +13,7 @@ const { AnalyticsTracker } = require('../analytics/tracker');
|
|
|
12
13
|
const { SmartDetector } = require('../smart/detector');
|
|
13
14
|
const { setHeaders } = require('../middleware/headers');
|
|
14
15
|
const { checkAccess } = require('./accessControl');
|
|
16
|
+
const { PrometheusFormatter } = require('../analytics/prometheus');
|
|
15
17
|
|
|
16
18
|
const STRATEGY_MAP = {
|
|
17
19
|
'fixed-window': fixedWindowCheck,
|
|
@@ -32,11 +34,15 @@ const STRATEGY_MAP = {
|
|
|
32
34
|
* // Programmatic check
|
|
33
35
|
* const result = await limiter.check('user:42');
|
|
34
36
|
*/
|
|
35
|
-
class Limiter {
|
|
37
|
+
class Limiter extends EventEmitter {
|
|
36
38
|
/**
|
|
37
39
|
* @param {object} options - NextLimiter configuration (see config.js for defaults)
|
|
38
40
|
*/
|
|
39
41
|
constructor(options = {}) {
|
|
42
|
+
super();
|
|
43
|
+
this.setMaxListeners(50);
|
|
44
|
+
this.on('error', () => {}); // default no-op handler prevents crashes
|
|
45
|
+
|
|
40
46
|
this._config = resolveConfig(options);
|
|
41
47
|
|
|
42
48
|
// Storage backend
|
|
@@ -63,7 +69,14 @@ class Limiter {
|
|
|
63
69
|
this._analytics = new AnalyticsTracker();
|
|
64
70
|
|
|
65
71
|
// Smart detector
|
|
66
|
-
this._smart = this._config.smart ? new SmartDetector(this._config) : null;
|
|
72
|
+
this._smart = this._config.smart ? new SmartDetector(this._config, this) : null;
|
|
73
|
+
|
|
74
|
+
// Stats event interval
|
|
75
|
+
if (this._config.statsInterval) {
|
|
76
|
+
this._statsTimer = setInterval(() => {
|
|
77
|
+
this.emit('stats', this.getStats());
|
|
78
|
+
}, this._config.statsInterval).unref();
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
@@ -86,7 +99,7 @@ class Limiter {
|
|
|
86
99
|
|
|
87
100
|
// ── Access control (whitelist / blacklist) ───────────────────────────
|
|
88
101
|
const clientIp = extractIp(req);
|
|
89
|
-
const access = checkAccess(clientIp, this._config);
|
|
102
|
+
const access = checkAccess(clientIp, this._config, this);
|
|
90
103
|
|
|
91
104
|
if (access.action === 'block') {
|
|
92
105
|
return res.status(403).json({
|
|
@@ -105,13 +118,17 @@ class Limiter {
|
|
|
105
118
|
const key = `${this._config.keyPrefix}${rawKey}`;
|
|
106
119
|
|
|
107
120
|
const result = await this._runCheck(key);
|
|
121
|
+
|
|
122
|
+
// Emit events
|
|
123
|
+
if (result.allowed) this.emit('allowed', rawKey, result);
|
|
124
|
+
else this.emit('blocked', rawKey, result);
|
|
108
125
|
|
|
109
126
|
// Record analytics
|
|
110
127
|
this._analytics.record(key, result.allowed);
|
|
111
128
|
|
|
112
129
|
// Set headers
|
|
113
130
|
if (this._config.headers) {
|
|
114
|
-
setHeaders(res, result
|
|
131
|
+
setHeaders(res, result);
|
|
115
132
|
}
|
|
116
133
|
|
|
117
134
|
if (!result.allowed) {
|
|
@@ -139,13 +156,49 @@ class Limiter {
|
|
|
139
156
|
|
|
140
157
|
next();
|
|
141
158
|
} catch (err) {
|
|
159
|
+
this.emit('error', err);
|
|
142
160
|
// Never let rate limiter errors take down the application
|
|
143
|
-
this._log.warn(`
|
|
161
|
+
this._log.warn(`Middleware error: ${err.message}. Failing open.`);
|
|
144
162
|
next();
|
|
145
163
|
}
|
|
146
164
|
};
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Express route handler for serving Prometheus metrics.
|
|
169
|
+
* Exposes getStats() data in plain text format (version=0.0.4).
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* app.get('/metrics', limiter.metricsHandler());
|
|
173
|
+
*/
|
|
174
|
+
metricsHandler() {
|
|
175
|
+
return (req, res) => {
|
|
176
|
+
try {
|
|
177
|
+
const formatter = new PrometheusFormatter(this);
|
|
178
|
+
res.set('Content-Type', formatter.contentType());
|
|
179
|
+
res.send(formatter.format());
|
|
180
|
+
} catch (err) {
|
|
181
|
+
res.status(500).type('text/plain').send(`Error generating metrics: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Global middleware that automatically intercepts GET /metrics requests
|
|
188
|
+
* and serves the Prometheus exposition format. Passes through all other routes.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* app.use(limiter.metricsMiddleware());
|
|
192
|
+
*/
|
|
193
|
+
metricsMiddleware() {
|
|
194
|
+
return (req, res, next) => {
|
|
195
|
+
if (req.method === 'GET' && req.path === '/metrics') {
|
|
196
|
+
return this.metricsHandler()(req, res);
|
|
197
|
+
}
|
|
198
|
+
next();
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
149
202
|
/**
|
|
150
203
|
* Programmatic rate limit check — use outside of HTTP middleware context.
|
|
151
204
|
*
|
|
@@ -160,7 +213,7 @@ class Limiter {
|
|
|
160
213
|
// Apply access control if the key looks like a plain IP address
|
|
161
214
|
const looksLikeIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(String(key).trim());
|
|
162
215
|
if (looksLikeIp) {
|
|
163
|
-
const access = checkAccess(key, this._config);
|
|
216
|
+
const access = checkAccess(key, this._config, this);
|
|
164
217
|
if (access.action === 'block') {
|
|
165
218
|
return {
|
|
166
219
|
allowed: false,
|
|
@@ -193,9 +246,35 @@ class Limiter {
|
|
|
193
246
|
const fullKey = `${this._config.keyPrefix}${key}`;
|
|
194
247
|
const result = await this._runCheck(fullKey);
|
|
195
248
|
this._analytics.record(fullKey, result.allowed);
|
|
249
|
+
|
|
250
|
+
if (result.allowed) this.emit('allowed', key, result);
|
|
251
|
+
else this.emit('blocked', key, result);
|
|
252
|
+
|
|
196
253
|
return result;
|
|
197
254
|
}
|
|
198
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Reset rate limit state for a key.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} key
|
|
260
|
+
*/
|
|
261
|
+
resetKey(key) {
|
|
262
|
+
const fullKey = `${this._config.keyPrefix}${key}`;
|
|
263
|
+
this._store.delete(fullKey);
|
|
264
|
+
this.emit('reset', key);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Cleanly dispose of stats cycles and store buffers.
|
|
269
|
+
*/
|
|
270
|
+
destroy() {
|
|
271
|
+
if (this._statsTimer) clearInterval(this._statsTimer);
|
|
272
|
+
if (this._store && typeof this._store.destroy === 'function') {
|
|
273
|
+
this._store.destroy();
|
|
274
|
+
}
|
|
275
|
+
this.removeAllListeners();
|
|
276
|
+
}
|
|
277
|
+
|
|
199
278
|
/**
|
|
200
279
|
* Manually reset the rate limit for a specific key.
|
|
201
280
|
* Useful after a user upgrades their plan, or for admin overrides.
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { PRESETS, DEFAULT_PLANS } = require('./core/config');
|
|
|
5
5
|
const { MemoryStore } = require('./store/memoryStore');
|
|
6
6
|
const { RedisStore } = require('./store/redisStore');
|
|
7
7
|
const { ipMatchesCidr, ipMatchesList } = require('./utils/cidr');
|
|
8
|
+
const { PrometheusFormatter } = require('./analytics/prometheus');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Create a fully configured rate limiter instance.
|
|
@@ -107,6 +108,9 @@ module.exports = {
|
|
|
107
108
|
ipMatchesCidr,
|
|
108
109
|
ipMatchesList,
|
|
109
110
|
|
|
111
|
+
// Analytics export
|
|
112
|
+
PrometheusFormatter,
|
|
113
|
+
|
|
110
114
|
// Constants
|
|
111
115
|
PRESETS,
|
|
112
116
|
DEFAULT_PLANS,
|
package/src/smart/detector.js
CHANGED
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
class SmartDetector {
|
|
19
19
|
/**
|
|
20
20
|
* @param {object} config - Resolved NextLimiter config
|
|
21
|
+
* @param {import('events').EventEmitter} [emitter] - Optional event emitter
|
|
21
22
|
*/
|
|
22
|
-
constructor(config) {
|
|
23
|
+
constructor(config, emitter) {
|
|
23
24
|
this._config = config;
|
|
25
|
+
this._emitter = emitter;
|
|
26
|
+
this._penalizedSet = new Set();
|
|
24
27
|
|
|
25
28
|
// Map: key → { windowStart, count, penalized, penaltyExpires }
|
|
26
29
|
/** @type {Map<string, object>} */
|
|
@@ -64,6 +67,17 @@ class SmartDetector {
|
|
|
64
67
|
if (state.count > normalRatePerObsWindow * config.smartThreshold) {
|
|
65
68
|
state.penalized = true;
|
|
66
69
|
state.penaltyExpires = now + config.smartCooldownMs;
|
|
70
|
+
|
|
71
|
+
if (this._emitter && !this._penalizedSet.has(key)) {
|
|
72
|
+
this._penalizedSet.add(key);
|
|
73
|
+
this._emitter.emit('penalized', key, {
|
|
74
|
+
key,
|
|
75
|
+
normalLimit: config.max,
|
|
76
|
+
reducedLimit: Math.floor(config.max * config.smartPenaltyFactor),
|
|
77
|
+
cooldownMs: config.smartCooldownMs,
|
|
78
|
+
detectedAt: new Date().toISOString()
|
|
79
|
+
});
|
|
80
|
+
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
// Reset observation window
|
|
@@ -84,6 +98,7 @@ class SmartDetector {
|
|
|
84
98
|
// Penalty expired — lift it
|
|
85
99
|
if (state.penalized && now >= state.penaltyExpires) {
|
|
86
100
|
state.penalized = false;
|
|
101
|
+
this._penalizedSet.delete(key);
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
return { penalized: false, effectiveMax: config.max };
|
|
@@ -111,6 +126,7 @@ class SmartDetector {
|
|
|
111
126
|
/** Clear all tracking state. */
|
|
112
127
|
reset() {
|
|
113
128
|
this._state.clear();
|
|
129
|
+
this._penalizedSet.clear();
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
package/types/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// NextLimiter — TypeScript type definitions
|
|
2
2
|
// Compatible with @types/node and @types/express
|
|
3
|
-
|
|
4
3
|
import { Request, Response, NextFunction } from 'express';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
5
|
|
|
6
6
|
// ── Store interface ──────────────────────────────────────────────────────────
|
|
7
7
|
|
|
@@ -102,6 +102,9 @@ export interface LimiterOptions {
|
|
|
102
102
|
|
|
103
103
|
/** Array of IPs or CIDR ranges to block immediately (403) */
|
|
104
104
|
blacklist?: string[];
|
|
105
|
+
|
|
106
|
+
/** ms interval to emit 'stats' event. undefined = disabled. */
|
|
107
|
+
statsInterval?: number;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
// ── Rate limit result ────────────────────────────────────────────────────────
|
|
@@ -139,6 +142,20 @@ export interface RateLimitResult {
|
|
|
139
142
|
|
|
140
143
|
// ── Analytics ────────────────────────────────────────────────────────────────
|
|
141
144
|
|
|
145
|
+
export interface PenaltyInfo {
|
|
146
|
+
key: string;
|
|
147
|
+
normalLimit: number;
|
|
148
|
+
reducedLimit: number;
|
|
149
|
+
cooldownMs: number;
|
|
150
|
+
detectedAt: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export declare class PrometheusFormatter {
|
|
154
|
+
constructor(limiter: Limiter);
|
|
155
|
+
format(): string;
|
|
156
|
+
contentType(): string;
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
export interface KeyCount {
|
|
143
160
|
key: string;
|
|
144
161
|
count: number;
|
|
@@ -175,18 +192,36 @@ export interface LimiterStats {
|
|
|
175
192
|
|
|
176
193
|
// ── Limiter class ────────────────────────────────────────────────────────────
|
|
177
194
|
|
|
178
|
-
export declare class Limiter {
|
|
195
|
+
export declare class Limiter extends EventEmitter {
|
|
179
196
|
constructor(options?: LimiterOptions);
|
|
180
197
|
|
|
181
198
|
/** Returns an Express-compatible middleware function */
|
|
182
199
|
middleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
183
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Express route handler for serving Prometheus metrics.
|
|
203
|
+
* Exposes getStats() data in plain text format (version=0.0.4).
|
|
204
|
+
*/
|
|
205
|
+
metricsHandler(): (req: any, res: any) => void;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Global middleware that automatically intercepts GET /metrics requests.
|
|
209
|
+
* Passes through all other routes.
|
|
210
|
+
*/
|
|
211
|
+
metricsMiddleware(): (req: any, res: any, next: any) => void;
|
|
212
|
+
|
|
184
213
|
/** Programmatic rate limit check by key string */
|
|
185
214
|
check(key: string): Promise<RateLimitResult>;
|
|
186
215
|
|
|
187
216
|
/** Reset rate limit state for a specific key */
|
|
188
217
|
reset(key: string): Promise<void>;
|
|
189
218
|
|
|
219
|
+
/** Reset rate limit state and clear from store immediately */
|
|
220
|
+
resetKey(key: string): void;
|
|
221
|
+
|
|
222
|
+
/** Clean up stores and timers */
|
|
223
|
+
destroy(): void;
|
|
224
|
+
|
|
190
225
|
/** Get analytics snapshot */
|
|
191
226
|
getStats(): LimiterStats;
|
|
192
227
|
|
|
@@ -195,6 +230,25 @@ export declare class Limiter {
|
|
|
195
230
|
|
|
196
231
|
/** Read-only resolved configuration */
|
|
197
232
|
readonly config: Readonly<Required<LimiterOptions>>;
|
|
233
|
+
|
|
234
|
+
// Typed EventEmitter overloads
|
|
235
|
+
on(event: 'blocked', listener: (key: string, result: RateLimitResult) => void): this;
|
|
236
|
+
on(event: 'allowed', listener: (key: string, result: RateLimitResult) => void): this;
|
|
237
|
+
on(event: 'penalized', listener: (key: string, info: PenaltyInfo) => void): this;
|
|
238
|
+
on(event: 'blacklisted', listener: (ip: string) => void): this;
|
|
239
|
+
on(event: 'whitelisted', listener: (ip: string) => void): this;
|
|
240
|
+
on(event: 'reset', listener: (key: string) => void): this;
|
|
241
|
+
on(event: 'stats', listener: (stats: LimiterStats) => void): this;
|
|
242
|
+
on(event: 'error', listener: (err: Error) => void): this;
|
|
243
|
+
|
|
244
|
+
once(event: 'blocked', listener: (key: string, result: RateLimitResult) => void): this;
|
|
245
|
+
once(event: 'allowed', listener: (key: string, result: RateLimitResult) => void): this;
|
|
246
|
+
once(event: 'penalized', listener: (key: string, info: PenaltyInfo) => void): this;
|
|
247
|
+
once(event: 'blacklisted', listener: (ip: string) => void): this;
|
|
248
|
+
once(event: 'whitelisted', listener: (ip: string) => void): this;
|
|
249
|
+
once(event: 'reset', listener: (key: string) => void): this;
|
|
250
|
+
once(event: 'stats', listener: (stats: LimiterStats) => void): this;
|
|
251
|
+
once(event: 'error', listener: (err: Error) => void): this;
|
|
198
252
|
}
|
|
199
253
|
|
|
200
254
|
// ── Factory functions ────────────────────────────────────────────────────────
|