fluxguard 0.1.1
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/LICENSE +21 -0
- package/dist/index.cjs +555 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.ts +125 -0
- package/dist/index.js +522 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
- package/scripts/fixed_window.lua +15 -0
- package/scripts/sliding_window_counter.lua +22 -0
- package/scripts/sliding_window_log.lua +20 -0
- package/scripts/token_bucket.lua +26 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var Algorithm = /* @__PURE__ */ ((Algorithm2) => {
|
|
3
|
+
Algorithm2["FIXED_WINDOW"] = "FIXED_WINDOW";
|
|
4
|
+
Algorithm2["SLIDING_WINDOW_LOG"] = "SLIDING_WINDOW_LOG";
|
|
5
|
+
Algorithm2["SLIDING_WINDOW_COUNTER"] = "SLIDING_WINDOW_COUNTER";
|
|
6
|
+
Algorithm2["TOKEN_BUCKET"] = "TOKEN_BUCKET";
|
|
7
|
+
return Algorithm2;
|
|
8
|
+
})(Algorithm || {});
|
|
9
|
+
|
|
10
|
+
// src/engine/localChecks.ts
|
|
11
|
+
function baseResult(algorithm, limit, allowed, remaining, resetMs, retryAfterMs) {
|
|
12
|
+
return {
|
|
13
|
+
allowed,
|
|
14
|
+
limit,
|
|
15
|
+
remaining: Math.max(0, remaining),
|
|
16
|
+
resetMs,
|
|
17
|
+
retryAfterMs,
|
|
18
|
+
algorithm
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function checkFixedWindowLocal(store, prefix, id, limit, windowMs, now) {
|
|
22
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
23
|
+
const k = `${prefix}${id}:${windowStart}`;
|
|
24
|
+
const raw = await store.get(k);
|
|
25
|
+
const count = raw ? parseInt(raw, 10) : 0;
|
|
26
|
+
const next = count + 1;
|
|
27
|
+
const resetMs = windowStart + windowMs;
|
|
28
|
+
if (next > limit) {
|
|
29
|
+
return baseResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, false, 0, resetMs, resetMs - now);
|
|
30
|
+
}
|
|
31
|
+
await store.set(k, String(next), windowMs * 2);
|
|
32
|
+
return baseResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, true, limit - next, resetMs);
|
|
33
|
+
}
|
|
34
|
+
async function checkTokenBucketLocal(store, prefix, id, limit, windowMs, now) {
|
|
35
|
+
const capacity = limit;
|
|
36
|
+
const refillPerMs = limit / windowMs;
|
|
37
|
+
const tokensKey = `${prefix}${id}:tokens`;
|
|
38
|
+
const lastKey = `${prefix}${id}:last_refill`;
|
|
39
|
+
const rawT = await store.get(tokensKey);
|
|
40
|
+
const rawL = await store.get(lastKey);
|
|
41
|
+
let tokens = rawT != null ? parseFloat(rawT) : capacity;
|
|
42
|
+
const last = rawL != null ? parseInt(rawL, 10) : now;
|
|
43
|
+
const delta = Math.max(0, now - last);
|
|
44
|
+
tokens = Math.min(capacity, tokens + delta * refillPerMs);
|
|
45
|
+
if (tokens < 1) {
|
|
46
|
+
const need = 1 - tokens;
|
|
47
|
+
const retryAfterMs = Math.ceil(need / refillPerMs);
|
|
48
|
+
const fullReset2 = now + Math.ceil(capacity / refillPerMs);
|
|
49
|
+
return baseResult(
|
|
50
|
+
"TOKEN_BUCKET" /* TOKEN_BUCKET */,
|
|
51
|
+
limit,
|
|
52
|
+
false,
|
|
53
|
+
0,
|
|
54
|
+
fullReset2,
|
|
55
|
+
retryAfterMs
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
tokens -= 1;
|
|
59
|
+
await store.set(tokensKey, String(tokens), windowMs * 3);
|
|
60
|
+
await store.set(lastKey, String(now), windowMs * 3);
|
|
61
|
+
const remaining = Math.floor(tokens);
|
|
62
|
+
const fullReset = now + Math.ceil((capacity - tokens) / refillPerMs);
|
|
63
|
+
return baseResult("TOKEN_BUCKET" /* TOKEN_BUCKET */, limit, true, remaining, fullReset);
|
|
64
|
+
}
|
|
65
|
+
async function checkSlidingWindowLogLocal(store, prefix, id, limit, windowMs, now) {
|
|
66
|
+
const k = `${prefix}${id}:log`;
|
|
67
|
+
const raw = await store.get(k);
|
|
68
|
+
let entries = [];
|
|
69
|
+
if (raw) {
|
|
70
|
+
try {
|
|
71
|
+
entries = JSON.parse(raw);
|
|
72
|
+
} catch {
|
|
73
|
+
entries = [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const cutoff = now - windowMs;
|
|
77
|
+
entries = entries.filter((t) => t > cutoff).sort((a, b) => a - b);
|
|
78
|
+
const resetMs = entries.length > 0 ? entries[0] + windowMs : now + windowMs;
|
|
79
|
+
if (entries.length >= limit) {
|
|
80
|
+
const oldest = entries[0];
|
|
81
|
+
return baseResult(
|
|
82
|
+
"SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */,
|
|
83
|
+
limit,
|
|
84
|
+
false,
|
|
85
|
+
0,
|
|
86
|
+
oldest + windowMs,
|
|
87
|
+
oldest + windowMs - now
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
entries.push(now);
|
|
91
|
+
await store.set(k, JSON.stringify(entries), windowMs * 2);
|
|
92
|
+
return baseResult("SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */, limit, true, limit - entries.length, resetMs);
|
|
93
|
+
}
|
|
94
|
+
async function checkSlidingWindowCounterLocal(store, prefix, id, limit, windowMs, now) {
|
|
95
|
+
const currStart = Math.floor(now / windowMs) * windowMs;
|
|
96
|
+
const prevStart = currStart - windowMs;
|
|
97
|
+
const kPrev = `${prefix}${id}:${prevStart}`;
|
|
98
|
+
const kCurr = `${prefix}${id}:${currStart}`;
|
|
99
|
+
const prev = parseInt(await store.get(kPrev) ?? "0", 10) || 0;
|
|
100
|
+
const curr = parseInt(await store.get(kCurr) ?? "0", 10) || 0;
|
|
101
|
+
const elapsed = now % windowMs;
|
|
102
|
+
const weight = 1 - elapsed / windowMs;
|
|
103
|
+
const estimated = Math.floor(prev * weight) + curr;
|
|
104
|
+
const resetMs = currStart + windowMs;
|
|
105
|
+
if (estimated >= limit) {
|
|
106
|
+
return baseResult(
|
|
107
|
+
"SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */,
|
|
108
|
+
limit,
|
|
109
|
+
false,
|
|
110
|
+
0,
|
|
111
|
+
resetMs,
|
|
112
|
+
resetMs - now
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
await store.set(kCurr, String(curr + 1), windowMs * 2);
|
|
116
|
+
await store.set(kPrev, String(prev), windowMs * 2);
|
|
117
|
+
const nextEstimated = estimated + 1;
|
|
118
|
+
return baseResult(
|
|
119
|
+
"SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */,
|
|
120
|
+
limit,
|
|
121
|
+
true,
|
|
122
|
+
Math.max(0, limit - nextEstimated),
|
|
123
|
+
resetMs
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/engine/redisChecks.ts
|
|
128
|
+
import { randomBytes } from "crypto";
|
|
129
|
+
function toResult(algorithm, limit, row, now) {
|
|
130
|
+
const allowed = Number(row[0]) === 1;
|
|
131
|
+
const remaining = Number(row[1]);
|
|
132
|
+
const resetMs = Number(row[3]);
|
|
133
|
+
const retryAfterMs = row[4] != null && Number(row[4]) > 0 ? Number(row[4]) : void 0;
|
|
134
|
+
return {
|
|
135
|
+
allowed,
|
|
136
|
+
limit,
|
|
137
|
+
remaining: Math.max(0, Math.min(limit, remaining)),
|
|
138
|
+
resetMs: Number.isFinite(resetMs) ? resetMs : now,
|
|
139
|
+
retryAfterMs,
|
|
140
|
+
algorithm
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function runRedisCheck(store, prefix, id, algorithm, limit, windowMs, now) {
|
|
144
|
+
switch (algorithm) {
|
|
145
|
+
case "FIXED_WINDOW" /* FIXED_WINDOW */:
|
|
146
|
+
return fixedWindow(store, prefix, id, limit, windowMs, now);
|
|
147
|
+
case "TOKEN_BUCKET" /* TOKEN_BUCKET */:
|
|
148
|
+
return tokenBucket(store, prefix, id, limit, windowMs, now);
|
|
149
|
+
case "SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */:
|
|
150
|
+
return slidingLog(store, prefix, id, limit, windowMs, now);
|
|
151
|
+
case "SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */:
|
|
152
|
+
return slidingCounter(store, prefix, id, limit, windowMs, now);
|
|
153
|
+
default:
|
|
154
|
+
throw new Error(`Unsupported algorithm: ${String(algorithm)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function fixedWindow(store, prefix, id, limit, windowMs, now) {
|
|
158
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
159
|
+
const key = `${prefix}${id}:${windowStart}`;
|
|
160
|
+
const script = store.loadScriptFile("fixed_window.lua");
|
|
161
|
+
const raw = await store.evalScript(script, [key], [limit, windowMs, now, windowStart]);
|
|
162
|
+
return toResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, raw, now);
|
|
163
|
+
}
|
|
164
|
+
async function tokenBucket(store, prefix, id, limit, windowMs, now) {
|
|
165
|
+
const k1 = `${prefix}${id}:tokens`;
|
|
166
|
+
const k2 = `${prefix}${id}:last_refill`;
|
|
167
|
+
const script = store.loadScriptFile("token_bucket.lua");
|
|
168
|
+
const raw = await store.evalScript(script, [k1, k2], [limit, windowMs, now]);
|
|
169
|
+
return toResult("TOKEN_BUCKET" /* TOKEN_BUCKET */, limit, raw, now);
|
|
170
|
+
}
|
|
171
|
+
async function slidingLog(store, prefix, id, limit, windowMs, now) {
|
|
172
|
+
const key = `${prefix}${id}:log`;
|
|
173
|
+
const member = `${now}-${randomBytes(8).toString("hex")}`;
|
|
174
|
+
const script = store.loadScriptFile("sliding_window_log.lua");
|
|
175
|
+
const raw = await store.evalScript(script, [key], [limit, windowMs, now, member]);
|
|
176
|
+
return toResult("SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */, limit, raw, now);
|
|
177
|
+
}
|
|
178
|
+
async function slidingCounter(store, prefix, id, limit, windowMs, now) {
|
|
179
|
+
const currStart = Math.floor(now / windowMs) * windowMs;
|
|
180
|
+
const prevStart = currStart - windowMs;
|
|
181
|
+
const kPrev = `${prefix}${id}:${prevStart}`;
|
|
182
|
+
const kCurr = `${prefix}${id}:${currStart}`;
|
|
183
|
+
const script = store.loadScriptFile("sliding_window_counter.lua");
|
|
184
|
+
const raw = await store.evalScript(script, [kPrev, kCurr], [limit, windowMs, now]);
|
|
185
|
+
return toResult("SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */, limit, raw, now);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/store/LocalStore.ts
|
|
189
|
+
var LocalStore = class {
|
|
190
|
+
constructor(clock = () => Date.now()) {
|
|
191
|
+
this.clock = clock;
|
|
192
|
+
}
|
|
193
|
+
clock;
|
|
194
|
+
data = /* @__PURE__ */ new Map();
|
|
195
|
+
async get(key) {
|
|
196
|
+
const e = this.data.get(key);
|
|
197
|
+
if (!e) return null;
|
|
198
|
+
const now = this.clock();
|
|
199
|
+
if (e.expiresAt > 0 && now > e.expiresAt) {
|
|
200
|
+
this.data.delete(key);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return e.value;
|
|
204
|
+
}
|
|
205
|
+
async set(key, value, ttlMs) {
|
|
206
|
+
const expiresAt = ttlMs > 0 ? this.clock() + ttlMs : 0;
|
|
207
|
+
this.data.set(key, { value, expiresAt });
|
|
208
|
+
}
|
|
209
|
+
async del(key) {
|
|
210
|
+
this.data.delete(key);
|
|
211
|
+
}
|
|
212
|
+
async evalScript() {
|
|
213
|
+
throw new Error("LocalStore does not support evalScript \u2014 use algorithm local path");
|
|
214
|
+
}
|
|
215
|
+
/** Test helper: clear all keys */
|
|
216
|
+
_clear() {
|
|
217
|
+
this.data.clear();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/store/RedisStore.ts
|
|
222
|
+
import { existsSync, readFileSync } from "fs";
|
|
223
|
+
import { dirname, join } from "path";
|
|
224
|
+
import { fileURLToPath } from "url";
|
|
225
|
+
import { Redis } from "ioredis";
|
|
226
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
227
|
+
function scriptPath(name) {
|
|
228
|
+
const fromDist = join(__dirname, "..", "scripts", name);
|
|
229
|
+
const fromSourceTree = join(__dirname, "..", "..", "scripts", name);
|
|
230
|
+
if (existsSync(fromDist)) return fromDist;
|
|
231
|
+
if (existsSync(fromSourceTree)) return fromSourceTree;
|
|
232
|
+
throw new Error(
|
|
233
|
+
`FluxGuard: Lua script not found: ${name} (tried ${fromDist} and ${fromSourceTree})`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
var RedisStore = class {
|
|
237
|
+
redis;
|
|
238
|
+
owns;
|
|
239
|
+
shaCache = /* @__PURE__ */ new Map();
|
|
240
|
+
constructor(redis, optionsIsObject) {
|
|
241
|
+
if (optionsIsObject) {
|
|
242
|
+
this.redis = new Redis(redis);
|
|
243
|
+
this.owns = true;
|
|
244
|
+
} else {
|
|
245
|
+
this.redis = redis;
|
|
246
|
+
this.owns = false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
loadScriptFile(filename) {
|
|
250
|
+
return readFileSync(scriptPath(filename), "utf8");
|
|
251
|
+
}
|
|
252
|
+
disconnect() {
|
|
253
|
+
if (this.owns) {
|
|
254
|
+
void this.redis.quit();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
get redisClient() {
|
|
258
|
+
return this.redis;
|
|
259
|
+
}
|
|
260
|
+
async get(key) {
|
|
261
|
+
const v = await this.redis.get(key);
|
|
262
|
+
return v;
|
|
263
|
+
}
|
|
264
|
+
async set(key, value, ttlMs) {
|
|
265
|
+
if (ttlMs > 0) {
|
|
266
|
+
await this.redis.set(key, value, "PX", ttlMs);
|
|
267
|
+
} else {
|
|
268
|
+
await this.redis.set(key, value);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async del(key) {
|
|
272
|
+
await this.redis.del(key);
|
|
273
|
+
}
|
|
274
|
+
async evalScript(script, keys, args) {
|
|
275
|
+
let sha = this.shaCache.get(script);
|
|
276
|
+
if (!sha) {
|
|
277
|
+
sha = await this.redis.script("LOAD", script);
|
|
278
|
+
this.shaCache.set(script, sha);
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const r = await this.redis.evalsha(
|
|
282
|
+
sha,
|
|
283
|
+
keys.length,
|
|
284
|
+
...keys,
|
|
285
|
+
...args.map((a) => String(a))
|
|
286
|
+
);
|
|
287
|
+
return normalizeEvalResult(r);
|
|
288
|
+
} catch (e) {
|
|
289
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
290
|
+
if (msg.includes("NOSCRIPT")) {
|
|
291
|
+
sha = await this.redis.script("LOAD", script);
|
|
292
|
+
this.shaCache.set(script, sha);
|
|
293
|
+
const r = await this.redis.evalsha(
|
|
294
|
+
sha,
|
|
295
|
+
keys.length,
|
|
296
|
+
...keys,
|
|
297
|
+
...args.map((a) => String(a))
|
|
298
|
+
);
|
|
299
|
+
return normalizeEvalResult(r);
|
|
300
|
+
}
|
|
301
|
+
throw e;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
function normalizeEvalResult(r) {
|
|
306
|
+
if (Array.isArray(r)) {
|
|
307
|
+
return r;
|
|
308
|
+
}
|
|
309
|
+
if (r == null) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
return [r];
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/FluxGuard.ts
|
|
316
|
+
var FluxGuard = class {
|
|
317
|
+
constructor(cfg) {
|
|
318
|
+
this.cfg = cfg;
|
|
319
|
+
this.config = cfg;
|
|
320
|
+
this.prefix = cfg.keyPrefix ?? "fluxguard:";
|
|
321
|
+
this.nowFn = cfg.nowFn ?? (() => Date.now());
|
|
322
|
+
if (cfg.redis) {
|
|
323
|
+
const r = cfg.redis;
|
|
324
|
+
const isClient = "connect" in r && typeof r.connect === "function";
|
|
325
|
+
this.store = new RedisStore(cfg.redis, !isClient);
|
|
326
|
+
} else {
|
|
327
|
+
this.store = new LocalStore(this.nowFn);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
cfg;
|
|
331
|
+
store;
|
|
332
|
+
prefix;
|
|
333
|
+
nowFn;
|
|
334
|
+
config;
|
|
335
|
+
metrics = {
|
|
336
|
+
totalChecks: 0,
|
|
337
|
+
allowed: 0,
|
|
338
|
+
throttled: 0,
|
|
339
|
+
redisErrors: 0
|
|
340
|
+
};
|
|
341
|
+
getMetrics() {
|
|
342
|
+
return { ...this.metrics };
|
|
343
|
+
}
|
|
344
|
+
/** Merge event recording for external metrics (e.g. Prometheus). */
|
|
345
|
+
onMetricsRecord(fn) {
|
|
346
|
+
const prev = this.config.metrics?.record;
|
|
347
|
+
this.config.metrics = {
|
|
348
|
+
...this.config.metrics,
|
|
349
|
+
record: (s) => {
|
|
350
|
+
prev?.(s);
|
|
351
|
+
fn(s);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
async check(key) {
|
|
356
|
+
this.metrics.totalChecks += 1;
|
|
357
|
+
try {
|
|
358
|
+
let result;
|
|
359
|
+
if (this.store instanceof RedisStore) {
|
|
360
|
+
result = await runRedisCheck(
|
|
361
|
+
this.store,
|
|
362
|
+
this.prefix,
|
|
363
|
+
key,
|
|
364
|
+
this.cfg.algorithm,
|
|
365
|
+
this.cfg.limit,
|
|
366
|
+
this.cfg.windowMs,
|
|
367
|
+
this.nowFn()
|
|
368
|
+
);
|
|
369
|
+
} else {
|
|
370
|
+
result = await this.checkLocal(key);
|
|
371
|
+
}
|
|
372
|
+
if (result.allowed) {
|
|
373
|
+
this.metrics.allowed += 1;
|
|
374
|
+
this.config.events?.onAllowed?.(key, result);
|
|
375
|
+
} else {
|
|
376
|
+
this.metrics.throttled += 1;
|
|
377
|
+
this.config.events?.onThrottled?.(key, result);
|
|
378
|
+
}
|
|
379
|
+
this.config.metrics?.record?.(this.getMetrics());
|
|
380
|
+
return result;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
if (this.store instanceof RedisStore) {
|
|
383
|
+
this.metrics.redisErrors += 1;
|
|
384
|
+
this.config.events?.onRedisError?.(err);
|
|
385
|
+
if (this.cfg.failOpen !== false) {
|
|
386
|
+
const now = this.nowFn();
|
|
387
|
+
const allowed = {
|
|
388
|
+
allowed: true,
|
|
389
|
+
limit: this.cfg.limit,
|
|
390
|
+
remaining: this.cfg.limit,
|
|
391
|
+
resetMs: now + this.cfg.windowMs,
|
|
392
|
+
algorithm: this.cfg.algorithm
|
|
393
|
+
};
|
|
394
|
+
this.metrics.allowed += 1;
|
|
395
|
+
this.config.metrics?.record?.(this.getMetrics());
|
|
396
|
+
return allowed;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async checkLocal(key) {
|
|
403
|
+
const now = this.nowFn();
|
|
404
|
+
const { algorithm, limit, windowMs } = this.cfg;
|
|
405
|
+
switch (algorithm) {
|
|
406
|
+
case "FIXED_WINDOW" /* FIXED_WINDOW */:
|
|
407
|
+
return checkFixedWindowLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
408
|
+
case "TOKEN_BUCKET" /* TOKEN_BUCKET */:
|
|
409
|
+
return checkTokenBucketLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
410
|
+
case "SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */:
|
|
411
|
+
return checkSlidingWindowLogLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
412
|
+
case "SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */:
|
|
413
|
+
return checkSlidingWindowCounterLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
414
|
+
default:
|
|
415
|
+
throw new Error(`Unsupported algorithm: ${String(algorithm)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// src/middleware/express.ts
|
|
421
|
+
var defaultKey = (req) => {
|
|
422
|
+
const xf = req.headers["x-forwarded-for"];
|
|
423
|
+
const ip = (typeof xf === "string" ? xf.split(",")[0]?.trim() : void 0) ?? req.socket.remoteAddress ?? "unknown";
|
|
424
|
+
return ip;
|
|
425
|
+
};
|
|
426
|
+
function fluxGuardMiddleware(limiter, options = {}) {
|
|
427
|
+
const keyExtract = options.keyExtract ?? defaultKey;
|
|
428
|
+
const sendHeaders = options.headers !== false;
|
|
429
|
+
return async (req, res, next) => {
|
|
430
|
+
const key = keyExtract(req);
|
|
431
|
+
let result;
|
|
432
|
+
try {
|
|
433
|
+
result = await limiter.check(key);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
next(e);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (sendHeaders) {
|
|
439
|
+
res.setHeader("X-RateLimit-Limit", String(result.limit));
|
|
440
|
+
res.setHeader("X-RateLimit-Remaining", String(result.remaining));
|
|
441
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil(result.resetMs / 1e3)));
|
|
442
|
+
if (!result.allowed && result.retryAfterMs != null) {
|
|
443
|
+
res.setHeader("Retry-After", String(Math.ceil(result.retryAfterMs / 1e3)));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!result.allowed) {
|
|
447
|
+
if (options.onThrottled) {
|
|
448
|
+
options.onThrottled(req, res, result);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
res.status(429).json({
|
|
452
|
+
error: "Too Many Requests",
|
|
453
|
+
limit: result.limit,
|
|
454
|
+
remaining: result.remaining,
|
|
455
|
+
resetMs: result.resetMs,
|
|
456
|
+
retryAfterMs: result.retryAfterMs
|
|
457
|
+
});
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
void options.skipSuccessfulRequests;
|
|
461
|
+
void options.skipFailedRequests;
|
|
462
|
+
next();
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/metrics/prometheus.ts
|
|
467
|
+
import { collectDefaultMetrics, Counter as PCounter, Registry as PRegistry } from "prom-client";
|
|
468
|
+
function createFluxGuardPrometheusMetrics(limiter, options = {}) {
|
|
469
|
+
const prefix = options.prefix ?? "fluxguard_";
|
|
470
|
+
const registry = new PRegistry();
|
|
471
|
+
if (options.collectDefault !== false) {
|
|
472
|
+
collectDefaultMetrics({ register: registry });
|
|
473
|
+
}
|
|
474
|
+
const checksTotal = new PCounter({
|
|
475
|
+
name: `${prefix}checks_total`,
|
|
476
|
+
help: "Total rate limit checks",
|
|
477
|
+
registers: [registry]
|
|
478
|
+
});
|
|
479
|
+
const allowedTotal = new PCounter({
|
|
480
|
+
name: `${prefix}allowed_total`,
|
|
481
|
+
help: "Allowed requests",
|
|
482
|
+
registers: [registry]
|
|
483
|
+
});
|
|
484
|
+
const throttledTotal = new PCounter({
|
|
485
|
+
name: `${prefix}throttled_total`,
|
|
486
|
+
help: "Throttled requests",
|
|
487
|
+
registers: [registry]
|
|
488
|
+
});
|
|
489
|
+
const redisErrorsTotal = new PCounter({
|
|
490
|
+
name: `${prefix}redis_errors_total`,
|
|
491
|
+
help: "Redis errors in rate limiter",
|
|
492
|
+
registers: [registry]
|
|
493
|
+
});
|
|
494
|
+
let last = limiter.getMetrics();
|
|
495
|
+
limiter.onMetricsRecord((s) => {
|
|
496
|
+
const dCheck = s.totalChecks - last.totalChecks;
|
|
497
|
+
const dAllowed = s.allowed - last.allowed;
|
|
498
|
+
const dThrottled = s.throttled - last.throttled;
|
|
499
|
+
const dErr = s.redisErrors - last.redisErrors;
|
|
500
|
+
last = { ...s };
|
|
501
|
+
if (dCheck > 0) checksTotal.inc(dCheck);
|
|
502
|
+
if (dAllowed > 0) allowedTotal.inc(dAllowed);
|
|
503
|
+
if (dThrottled > 0) throttledTotal.inc(dThrottled);
|
|
504
|
+
if (dErr > 0) redisErrorsTotal.inc(dErr);
|
|
505
|
+
});
|
|
506
|
+
return {
|
|
507
|
+
registry,
|
|
508
|
+
checksTotal,
|
|
509
|
+
allowedTotal,
|
|
510
|
+
throttledTotal,
|
|
511
|
+
redisErrorsTotal
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
export {
|
|
515
|
+
Algorithm,
|
|
516
|
+
FluxGuard,
|
|
517
|
+
LocalStore,
|
|
518
|
+
RedisStore,
|
|
519
|
+
createFluxGuardPrometheusMetrics,
|
|
520
|
+
fluxGuardMiddleware
|
|
521
|
+
};
|
|
522
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/engine/localChecks.ts","../src/engine/redisChecks.ts","../src/store/LocalStore.ts","../src/store/RedisStore.ts","../src/FluxGuard.ts","../src/middleware/express.ts","../src/metrics/prometheus.ts"],"sourcesContent":["import type { Redis as RedisClient, RedisOptions } from \"ioredis\";\n\nexport enum Algorithm {\n FIXED_WINDOW = \"FIXED_WINDOW\",\n SLIDING_WINDOW_LOG = \"SLIDING_WINDOW_LOG\",\n SLIDING_WINDOW_COUNTER = \"SLIDING_WINDOW_COUNTER\",\n TOKEN_BUCKET = \"TOKEN_BUCKET\",\n}\n\nexport type RateLimitResult = {\n allowed: boolean;\n limit: number;\n remaining: number;\n /** Epoch ms when the current window resets or limit fully replenishes (best effort). */\n resetMs: number;\n /** Suggested retry-after in ms when throttled. */\n retryAfterMs?: number;\n algorithm: Algorithm;\n};\n\nexport type FluxGuardMetricsSnapshot = {\n totalChecks: number;\n allowed: number;\n throttled: number;\n redisErrors: number;\n};\n\nexport type FluxGuardEvents = {\n onAllowed?: (key: string, result: RateLimitResult) => void;\n onThrottled?: (key: string, result: RateLimitResult) => void;\n onRedisError?: (err: unknown) => void;\n};\n\nexport type FluxGuardConfig = {\n algorithm: Algorithm;\n /** Max requests (or burst capacity for token bucket). */\n limit: number;\n windowMs: number;\n /** Redis connection — omit for in-memory limiting. */\n redis?: RedisOptions | RedisClient;\n keyPrefix?: string;\n /** Injected clock — default `Date.now`. */\n nowFn?: () => number;\n failOpen?: boolean;\n /** Optional external metrics hook — receives snapshot after each successful check path. */\n metrics?: { record?: (s: FluxGuardMetricsSnapshot) => void };\n events?: FluxGuardEvents;\n};\n","import type { Store } from \"../store/Store.js\";\nimport { Algorithm, type RateLimitResult } from \"../types.js\";\n\nfunction baseResult(\n algorithm: Algorithm,\n limit: number,\n allowed: boolean,\n remaining: number,\n resetMs: number,\n retryAfterMs?: number,\n): RateLimitResult {\n return {\n allowed,\n limit,\n remaining: Math.max(0, remaining),\n resetMs,\n retryAfterMs,\n algorithm,\n };\n}\n\nexport async function checkFixedWindowLocal(\n store: Store,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const k = `${prefix}${id}:${windowStart}`;\n const raw = await store.get(k);\n const count = raw ? parseInt(raw, 10) : 0;\n const next = count + 1;\n const resetMs = windowStart + windowMs;\n if (next > limit) {\n return baseResult(Algorithm.FIXED_WINDOW, limit, false, 0, resetMs, resetMs - now);\n }\n await store.set(k, String(next), windowMs * 2);\n return baseResult(Algorithm.FIXED_WINDOW, limit, true, limit - next, resetMs);\n}\n\nexport async function checkTokenBucketLocal(\n store: Store,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const capacity = limit;\n const refillPerMs = limit / windowMs;\n const tokensKey = `${prefix}${id}:tokens`;\n const lastKey = `${prefix}${id}:last_refill`;\n const rawT = await store.get(tokensKey);\n const rawL = await store.get(lastKey);\n let tokens = rawT != null ? parseFloat(rawT) : capacity;\n const last = rawL != null ? parseInt(rawL, 10) : now;\n const delta = Math.max(0, now - last);\n tokens = Math.min(capacity, tokens + delta * refillPerMs);\n if (tokens < 1) {\n const need = 1 - tokens;\n const retryAfterMs = Math.ceil(need / refillPerMs);\n const fullReset = now + Math.ceil(capacity / refillPerMs);\n return baseResult(\n Algorithm.TOKEN_BUCKET,\n limit,\n false,\n 0,\n fullReset,\n retryAfterMs,\n );\n }\n tokens -= 1;\n await store.set(tokensKey, String(tokens), windowMs * 3);\n await store.set(lastKey, String(now), windowMs * 3);\n const remaining = Math.floor(tokens);\n const fullReset = now + Math.ceil((capacity - tokens) / refillPerMs);\n return baseResult(Algorithm.TOKEN_BUCKET, limit, true, remaining, fullReset);\n}\n\nexport async function checkSlidingWindowLogLocal(\n store: Store,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const k = `${prefix}${id}:log`;\n const raw = await store.get(k);\n let entries: number[] = [];\n if (raw) {\n try {\n entries = JSON.parse(raw) as number[];\n } catch {\n entries = [];\n }\n }\n const cutoff = now - windowMs;\n entries = entries.filter((t) => t > cutoff).sort((a, b) => a - b);\n const resetMs = entries.length > 0 ? entries[0]! + windowMs : now + windowMs;\n if (entries.length >= limit) {\n const oldest = entries[0]!;\n return baseResult(\n Algorithm.SLIDING_WINDOW_LOG,\n limit,\n false,\n 0,\n oldest + windowMs,\n oldest + windowMs - now,\n );\n }\n entries.push(now);\n await store.set(k, JSON.stringify(entries), windowMs * 2);\n return baseResult(Algorithm.SLIDING_WINDOW_LOG, limit, true, limit - entries.length, resetMs);\n}\n\nexport async function checkSlidingWindowCounterLocal(\n store: Store,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const currStart = Math.floor(now / windowMs) * windowMs;\n const prevStart = currStart - windowMs;\n const kPrev = `${prefix}${id}:${prevStart}`;\n const kCurr = `${prefix}${id}:${currStart}`;\n const prev = parseInt((await store.get(kPrev)) ?? \"0\", 10) || 0;\n const curr = parseInt((await store.get(kCurr)) ?? \"0\", 10) || 0;\n const elapsed = now % windowMs;\n const weight = 1 - elapsed / windowMs;\n const estimated = Math.floor(prev * weight) + curr;\n const resetMs = currStart + windowMs;\n if (estimated >= limit) {\n return baseResult(\n Algorithm.SLIDING_WINDOW_COUNTER,\n limit,\n false,\n 0,\n resetMs,\n resetMs - now,\n );\n }\n await store.set(kCurr, String(curr + 1), windowMs * 2);\n await store.set(kPrev, String(prev), windowMs * 2);\n const nextEstimated = estimated + 1;\n return baseResult(\n Algorithm.SLIDING_WINDOW_COUNTER,\n limit,\n true,\n Math.max(0, limit - nextEstimated),\n resetMs,\n );\n}\n","import { randomBytes } from \"node:crypto\";\nimport { Algorithm, type RateLimitResult } from \"../types.js\";\nimport type { RedisStore } from \"../store/RedisStore.js\";\n\nfunction toResult(\n algorithm: Algorithm,\n limit: number,\n row: unknown[],\n now: number,\n): RateLimitResult {\n const allowed = Number(row[0]) === 1;\n const remaining = Number(row[1]);\n const resetMs = Number(row[3]);\n const retryAfterMs = row[4] != null && Number(row[4]) > 0 ? Number(row[4]) : undefined;\n return {\n allowed,\n limit,\n remaining: Math.max(0, Math.min(limit, remaining)),\n resetMs: Number.isFinite(resetMs) ? resetMs : now,\n retryAfterMs,\n algorithm,\n };\n}\n\nexport async function runRedisCheck(\n store: RedisStore,\n prefix: string,\n id: string,\n algorithm: Algorithm,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n switch (algorithm) {\n case Algorithm.FIXED_WINDOW:\n return fixedWindow(store, prefix, id, limit, windowMs, now);\n case Algorithm.TOKEN_BUCKET:\n return tokenBucket(store, prefix, id, limit, windowMs, now);\n case Algorithm.SLIDING_WINDOW_LOG:\n return slidingLog(store, prefix, id, limit, windowMs, now);\n case Algorithm.SLIDING_WINDOW_COUNTER:\n return slidingCounter(store, prefix, id, limit, windowMs, now);\n default:\n throw new Error(`Unsupported algorithm: ${String(algorithm)}`);\n }\n}\n\nasync function fixedWindow(\n store: RedisStore,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const windowStart = Math.floor(now / windowMs) * windowMs;\n const key = `${prefix}${id}:${windowStart}`;\n const script = store.loadScriptFile(\"fixed_window.lua\");\n const raw = await store.evalScript(script, [key], [limit, windowMs, now, windowStart]);\n return toResult(Algorithm.FIXED_WINDOW, limit, raw, now);\n}\n\nasync function tokenBucket(\n store: RedisStore,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const k1 = `${prefix}${id}:tokens`;\n const k2 = `${prefix}${id}:last_refill`;\n const script = store.loadScriptFile(\"token_bucket.lua\");\n const raw = await store.evalScript(script, [k1, k2], [limit, windowMs, now]);\n return toResult(Algorithm.TOKEN_BUCKET, limit, raw, now);\n}\n\nasync function slidingLog(\n store: RedisStore,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const key = `${prefix}${id}:log`;\n const member = `${now}-${randomBytes(8).toString(\"hex\")}`;\n const script = store.loadScriptFile(\"sliding_window_log.lua\");\n const raw = await store.evalScript(script, [key], [limit, windowMs, now, member]);\n return toResult(Algorithm.SLIDING_WINDOW_LOG, limit, raw, now);\n}\n\nasync function slidingCounter(\n store: RedisStore,\n prefix: string,\n id: string,\n limit: number,\n windowMs: number,\n now: number,\n): Promise<RateLimitResult> {\n const currStart = Math.floor(now / windowMs) * windowMs;\n const prevStart = currStart - windowMs;\n const kPrev = `${prefix}${id}:${prevStart}`;\n const kCurr = `${prefix}${id}:${currStart}`;\n const script = store.loadScriptFile(\"sliding_window_counter.lua\");\n const raw = await store.evalScript(script, [kPrev, kCurr], [limit, windowMs, now]);\n return toResult(Algorithm.SLIDING_WINDOW_COUNTER, limit, raw, now);\n}\n","import type { Store } from \"./Store.js\";\n\ntype Entry = { value: string; expiresAt: number };\n\nexport class LocalStore implements Store {\n private readonly data = new Map<string, Entry>();\n\n constructor(private readonly clock: () => number = () => Date.now()) {}\n\n async get(key: string): Promise<string | null> {\n const e = this.data.get(key);\n if (!e) return null;\n const now = this.clock();\n if (e.expiresAt > 0 && now > e.expiresAt) {\n this.data.delete(key);\n return null;\n }\n return e.value;\n }\n\n async set(key: string, value: string, ttlMs: number): Promise<void> {\n const expiresAt = ttlMs > 0 ? this.clock() + ttlMs : 0;\n this.data.set(key, { value, expiresAt });\n }\n\n async del(key: string): Promise<void> {\n this.data.delete(key);\n }\n\n async evalScript(): Promise<unknown[]> {\n throw new Error(\"LocalStore does not support evalScript — use algorithm local path\");\n }\n\n /** Test helper: clear all keys */\n _clear(): void {\n this.data.clear();\n }\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Redis } from \"ioredis\";\nimport type { RedisOptions } from \"ioredis\";\nimport type { Store } from \"./Store.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nfunction scriptPath(name: string): string {\n // Bundled entry: __dirname is `dist/` → `../scripts`. Tests (ts-jest): __dirname is `src/store/` → `../../scripts`.\n const fromDist = join(__dirname, \"..\", \"scripts\", name);\n const fromSourceTree = join(__dirname, \"..\", \"..\", \"scripts\", name);\n if (existsSync(fromDist)) return fromDist;\n if (existsSync(fromSourceTree)) return fromSourceTree;\n throw new Error(\n `FluxGuard: Lua script not found: ${name} (tried ${fromDist} and ${fromSourceTree})`,\n );\n}\n\nexport class RedisStore implements Store {\n private readonly redis: Redis;\n private readonly owns: boolean;\n private readonly shaCache = new Map<string, string>();\n\n constructor(redis: Redis | RedisOptions, optionsIsObject: boolean) {\n if (optionsIsObject) {\n this.redis = new Redis(redis as RedisOptions);\n this.owns = true;\n } else {\n this.redis = redis as Redis;\n this.owns = false;\n }\n }\n\n loadScriptFile(filename: string): string {\n return readFileSync(scriptPath(filename), \"utf8\");\n }\n\n disconnect(): void {\n if (this.owns) {\n void this.redis.quit();\n }\n }\n\n get redisClient(): Redis {\n return this.redis;\n }\n\n async get(key: string): Promise<string | null> {\n const v = await this.redis.get(key);\n return v;\n }\n\n async set(key: string, value: string, ttlMs: number): Promise<void> {\n if (ttlMs > 0) {\n await this.redis.set(key, value, \"PX\", ttlMs);\n } else {\n await this.redis.set(key, value);\n }\n }\n\n async del(key: string): Promise<void> {\n await this.redis.del(key);\n }\n\n async evalScript(script: string, keys: string[], args: (string | number)[]): Promise<unknown[]> {\n let sha = this.shaCache.get(script);\n if (!sha) {\n sha = (await this.redis.script(\"LOAD\", script)) as string;\n this.shaCache.set(script, sha);\n }\n try {\n const r = await this.redis.evalsha(\n sha,\n keys.length,\n ...keys,\n ...args.map((a) => String(a)),\n );\n return normalizeEvalResult(r);\n } catch (e: unknown) {\n const msg = e instanceof Error ? e.message : String(e);\n if (msg.includes(\"NOSCRIPT\")) {\n sha = (await this.redis.script(\"LOAD\", script)) as string;\n this.shaCache.set(script, sha);\n const r = await this.redis.evalsha(\n sha,\n keys.length,\n ...keys,\n ...args.map((a) => String(a)),\n );\n return normalizeEvalResult(r);\n }\n throw e;\n }\n }\n}\n\nfunction normalizeEvalResult(r: unknown): unknown[] {\n if (Array.isArray(r)) {\n return r as unknown[];\n }\n if (r == null) {\n return [];\n }\n return [r];\n}\n","import {\n checkFixedWindowLocal,\n checkSlidingWindowCounterLocal,\n checkSlidingWindowLogLocal,\n checkTokenBucketLocal,\n} from \"./engine/localChecks.js\";\nimport { runRedisCheck } from \"./engine/redisChecks.js\";\nimport { LocalStore } from \"./store/LocalStore.js\";\nimport { RedisStore } from \"./store/RedisStore.js\";\nimport type { Store } from \"./store/Store.js\";\nimport {\n Algorithm,\n type FluxGuardConfig,\n type FluxGuardMetricsSnapshot,\n type RateLimitResult,\n} from \"./types.js\";\n\nexport class FluxGuard {\n private readonly store: Store;\n private readonly prefix: string;\n private readonly nowFn: () => number;\n private readonly config: FluxGuardConfig;\n private metrics: FluxGuardMetricsSnapshot = {\n totalChecks: 0,\n allowed: 0,\n throttled: 0,\n redisErrors: 0,\n };\n\n constructor(private readonly cfg: FluxGuardConfig) {\n this.config = cfg;\n this.prefix = cfg.keyPrefix ?? \"fluxguard:\";\n this.nowFn = cfg.nowFn ?? (() => Date.now());\n if (cfg.redis) {\n const r = cfg.redis as object;\n const isClient = \"connect\" in r && typeof (r as { connect?: unknown }).connect === \"function\";\n this.store = new RedisStore(cfg.redis, !isClient);\n } else {\n this.store = new LocalStore(this.nowFn);\n }\n }\n\n getMetrics(): FluxGuardMetricsSnapshot {\n return { ...this.metrics };\n }\n\n /** Merge event recording for external metrics (e.g. Prometheus). */\n onMetricsRecord(fn: (snapshot: FluxGuardMetricsSnapshot) => void): void {\n const prev = this.config.metrics?.record;\n this.config.metrics = {\n ...this.config.metrics,\n record: (s) => {\n prev?.(s);\n fn(s);\n },\n };\n }\n\n async check(key: string): Promise<RateLimitResult> {\n this.metrics.totalChecks += 1;\n try {\n let result: RateLimitResult;\n if (this.store instanceof RedisStore) {\n result = await runRedisCheck(\n this.store,\n this.prefix,\n key,\n this.cfg.algorithm,\n this.cfg.limit,\n this.cfg.windowMs,\n this.nowFn(),\n );\n } else {\n result = await this.checkLocal(key);\n }\n if (result.allowed) {\n this.metrics.allowed += 1;\n this.config.events?.onAllowed?.(key, result);\n } else {\n this.metrics.throttled += 1;\n this.config.events?.onThrottled?.(key, result);\n }\n this.config.metrics?.record?.(this.getMetrics());\n return result;\n } catch (err) {\n if (this.store instanceof RedisStore) {\n this.metrics.redisErrors += 1;\n this.config.events?.onRedisError?.(err);\n if (this.cfg.failOpen !== false) {\n const now = this.nowFn();\n const allowed: RateLimitResult = {\n allowed: true,\n limit: this.cfg.limit,\n remaining: this.cfg.limit,\n resetMs: now + this.cfg.windowMs,\n algorithm: this.cfg.algorithm,\n };\n this.metrics.allowed += 1;\n this.config.metrics?.record?.(this.getMetrics());\n return allowed;\n }\n }\n throw err;\n }\n }\n\n private async checkLocal(key: string): Promise<RateLimitResult> {\n const now = this.nowFn();\n const { algorithm, limit, windowMs } = this.cfg;\n switch (algorithm) {\n case Algorithm.FIXED_WINDOW:\n return checkFixedWindowLocal(this.store, this.prefix, key, limit, windowMs, now);\n case Algorithm.TOKEN_BUCKET:\n return checkTokenBucketLocal(this.store, this.prefix, key, limit, windowMs, now);\n case Algorithm.SLIDING_WINDOW_LOG:\n return checkSlidingWindowLogLocal(this.store, this.prefix, key, limit, windowMs, now);\n case Algorithm.SLIDING_WINDOW_COUNTER:\n return checkSlidingWindowCounterLocal(this.store, this.prefix, key, limit, windowMs, now);\n default:\n throw new Error(`Unsupported algorithm: ${String(algorithm)}`);\n }\n }\n}\n","import type { RequestHandler, Request, Response } from \"express\";\nimport type { FluxGuard } from \"../FluxGuard.js\";\nimport type { RateLimitResult } from \"../types.js\";\n\nexport type ExpressLimiterOptions = {\n keyExtract?: (req: Request) => string;\n onThrottled?: (req: Request, res: Response, result: RateLimitResult) => void;\n skipSuccessfulRequests?: boolean;\n skipFailedRequests?: boolean;\n headers?: boolean;\n};\n\nconst defaultKey = (req: Request): string => {\n const xf = req.headers[\"x-forwarded-for\"];\n const ip =\n (typeof xf === \"string\" ? xf.split(\",\")[0]?.trim() : undefined) ??\n req.socket.remoteAddress ??\n \"unknown\";\n return ip;\n};\n\nexport function fluxGuardMiddleware(\n limiter: FluxGuard,\n options: ExpressLimiterOptions = {},\n): RequestHandler {\n const keyExtract = options.keyExtract ?? defaultKey;\n const sendHeaders = options.headers !== false;\n\n return async (req, res, next) => {\n const key = keyExtract(req);\n let result: RateLimitResult;\n try {\n result = await limiter.check(key);\n } catch (e) {\n next(e);\n return;\n }\n\n if (sendHeaders) {\n res.setHeader(\"X-RateLimit-Limit\", String(result.limit));\n res.setHeader(\"X-RateLimit-Remaining\", String(result.remaining));\n res.setHeader(\"X-RateLimit-Reset\", String(Math.ceil(result.resetMs / 1000)));\n if (!result.allowed && result.retryAfterMs != null) {\n res.setHeader(\"Retry-After\", String(Math.ceil(result.retryAfterMs / 1000)));\n }\n }\n\n if (!result.allowed) {\n if (options.onThrottled) {\n options.onThrottled(req, res, result);\n return;\n }\n res.status(429).json({\n error: \"Too Many Requests\",\n limit: result.limit,\n remaining: result.remaining,\n resetMs: result.resetMs,\n retryAfterMs: result.retryAfterMs,\n });\n return;\n }\n\n void options.skipSuccessfulRequests;\n void options.skipFailedRequests;\n\n next();\n };\n}\n","import type { Counter, Registry } from \"prom-client\";\nimport { collectDefaultMetrics, Counter as PCounter, Registry as PRegistry } from \"prom-client\";\nimport type { FluxGuard } from \"../FluxGuard.js\";\n\nexport type PrometheusMetrics = {\n registry: Registry;\n checksTotal: Counter<string>;\n allowedTotal: Counter<string>;\n throttledTotal: Counter<string>;\n redisErrorsTotal: Counter<string>;\n};\n\nexport function createFluxGuardPrometheusMetrics(\n limiter: FluxGuard,\n options: { prefix?: string; collectDefault?: boolean } = {},\n): PrometheusMetrics {\n const prefix = options.prefix ?? \"fluxguard_\";\n const registry = new PRegistry();\n if (options.collectDefault !== false) {\n collectDefaultMetrics({ register: registry });\n }\n\n const checksTotal = new PCounter({\n name: `${prefix}checks_total`,\n help: \"Total rate limit checks\",\n registers: [registry],\n });\n\n const allowedTotal = new PCounter({\n name: `${prefix}allowed_total`,\n help: \"Allowed requests\",\n registers: [registry],\n });\n\n const throttledTotal = new PCounter({\n name: `${prefix}throttled_total`,\n help: \"Throttled requests\",\n registers: [registry],\n });\n\n const redisErrorsTotal = new PCounter({\n name: `${prefix}redis_errors_total`,\n help: \"Redis errors in rate limiter\",\n registers: [registry],\n });\n\n let last = limiter.getMetrics();\n limiter.onMetricsRecord((s) => {\n const dCheck = s.totalChecks - last.totalChecks;\n const dAllowed = s.allowed - last.allowed;\n const dThrottled = s.throttled - last.throttled;\n const dErr = s.redisErrors - last.redisErrors;\n last = { ...s };\n if (dCheck > 0) checksTotal.inc(dCheck);\n if (dAllowed > 0) allowedTotal.inc(dAllowed);\n if (dThrottled > 0) throttledTotal.inc(dThrottled);\n if (dErr > 0) redisErrorsTotal.inc(dErr);\n });\n\n return {\n registry,\n checksTotal,\n allowedTotal,\n throttledTotal,\n redisErrorsTotal,\n };\n}\n"],"mappings":"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","names":["Algorithm","fullReset"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fluxguard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Distributed rate limiter for Node.js — Redis-backed with multiple algorithms",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/Zeeshan-2k1/fluxguard.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Zeeshan-2k1/fluxguard/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Zeeshan-2k1/fluxguard#readme",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"scripts",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"provenance": true
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"ioredis": ">=5.0.0",
|
|
39
|
+
"prom-client": ">=15.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"ioredis": {
|
|
43
|
+
"optional": true
|
|
44
|
+
},
|
|
45
|
+
"prom-client": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.17.0",
|
|
51
|
+
"@types/express": "^5.0.0",
|
|
52
|
+
"@types/jest": "^29.5.14",
|
|
53
|
+
"@types/node": "^22.10.2",
|
|
54
|
+
"@types/supertest": "^6.0.2",
|
|
55
|
+
"eslint": "^9.17.0",
|
|
56
|
+
"express": "^4.21.2",
|
|
57
|
+
"ioredis": "^5.4.2",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"prom-client": "^15.1.3",
|
|
60
|
+
"supertest": "^7.0.0",
|
|
61
|
+
"ts-jest": "^29.2.5",
|
|
62
|
+
"tsup": "^8.3.5",
|
|
63
|
+
"typescript": "^5.7.2",
|
|
64
|
+
"typescript-eslint": "^8.18.1",
|
|
65
|
+
"@fluxguard/fixtures": "0.1.0"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap --external ioredis --external prom-client",
|
|
69
|
+
"clean": "rm -rf dist",
|
|
70
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
71
|
+
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --testPathPattern=integration",
|
|
72
|
+
"lint": "eslint src --max-warnings 0"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
-- KEYS[1] = counter key for window
|
|
2
|
+
-- ARGV[1] = limit, ARGV[2] = window_ms, ARGV[3] = now_ms, ARGV[4] = window_start
|
|
3
|
+
local limit = tonumber(ARGV[1])
|
|
4
|
+
local window = tonumber(ARGV[2])
|
|
5
|
+
local current = tonumber(redis.call('GET', KEYS[1])) or 0
|
|
6
|
+
if current >= limit then
|
|
7
|
+
local reset = tonumber(ARGV[4]) + window
|
|
8
|
+
return {0, current, limit, reset}
|
|
9
|
+
end
|
|
10
|
+
local next = redis.call('INCR', KEYS[1])
|
|
11
|
+
if next == 1 then
|
|
12
|
+
redis.call('PEXPIRE', KEYS[1], window * 2)
|
|
13
|
+
end
|
|
14
|
+
local reset = tonumber(ARGV[4]) + window
|
|
15
|
+
return {1, limit - next, limit, reset}
|