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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FluxGuard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Algorithm: () => Algorithm,
|
|
24
|
+
FluxGuard: () => FluxGuard,
|
|
25
|
+
LocalStore: () => LocalStore,
|
|
26
|
+
RedisStore: () => RedisStore,
|
|
27
|
+
createFluxGuardPrometheusMetrics: () => createFluxGuardPrometheusMetrics,
|
|
28
|
+
fluxGuardMiddleware: () => fluxGuardMiddleware
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/types.ts
|
|
33
|
+
var Algorithm = /* @__PURE__ */ ((Algorithm2) => {
|
|
34
|
+
Algorithm2["FIXED_WINDOW"] = "FIXED_WINDOW";
|
|
35
|
+
Algorithm2["SLIDING_WINDOW_LOG"] = "SLIDING_WINDOW_LOG";
|
|
36
|
+
Algorithm2["SLIDING_WINDOW_COUNTER"] = "SLIDING_WINDOW_COUNTER";
|
|
37
|
+
Algorithm2["TOKEN_BUCKET"] = "TOKEN_BUCKET";
|
|
38
|
+
return Algorithm2;
|
|
39
|
+
})(Algorithm || {});
|
|
40
|
+
|
|
41
|
+
// src/engine/localChecks.ts
|
|
42
|
+
function baseResult(algorithm, limit, allowed, remaining, resetMs, retryAfterMs) {
|
|
43
|
+
return {
|
|
44
|
+
allowed,
|
|
45
|
+
limit,
|
|
46
|
+
remaining: Math.max(0, remaining),
|
|
47
|
+
resetMs,
|
|
48
|
+
retryAfterMs,
|
|
49
|
+
algorithm
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function checkFixedWindowLocal(store, prefix, id, limit, windowMs, now) {
|
|
53
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
54
|
+
const k = `${prefix}${id}:${windowStart}`;
|
|
55
|
+
const raw = await store.get(k);
|
|
56
|
+
const count = raw ? parseInt(raw, 10) : 0;
|
|
57
|
+
const next = count + 1;
|
|
58
|
+
const resetMs = windowStart + windowMs;
|
|
59
|
+
if (next > limit) {
|
|
60
|
+
return baseResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, false, 0, resetMs, resetMs - now);
|
|
61
|
+
}
|
|
62
|
+
await store.set(k, String(next), windowMs * 2);
|
|
63
|
+
return baseResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, true, limit - next, resetMs);
|
|
64
|
+
}
|
|
65
|
+
async function checkTokenBucketLocal(store, prefix, id, limit, windowMs, now) {
|
|
66
|
+
const capacity = limit;
|
|
67
|
+
const refillPerMs = limit / windowMs;
|
|
68
|
+
const tokensKey = `${prefix}${id}:tokens`;
|
|
69
|
+
const lastKey = `${prefix}${id}:last_refill`;
|
|
70
|
+
const rawT = await store.get(tokensKey);
|
|
71
|
+
const rawL = await store.get(lastKey);
|
|
72
|
+
let tokens = rawT != null ? parseFloat(rawT) : capacity;
|
|
73
|
+
const last = rawL != null ? parseInt(rawL, 10) : now;
|
|
74
|
+
const delta = Math.max(0, now - last);
|
|
75
|
+
tokens = Math.min(capacity, tokens + delta * refillPerMs);
|
|
76
|
+
if (tokens < 1) {
|
|
77
|
+
const need = 1 - tokens;
|
|
78
|
+
const retryAfterMs = Math.ceil(need / refillPerMs);
|
|
79
|
+
const fullReset2 = now + Math.ceil(capacity / refillPerMs);
|
|
80
|
+
return baseResult(
|
|
81
|
+
"TOKEN_BUCKET" /* TOKEN_BUCKET */,
|
|
82
|
+
limit,
|
|
83
|
+
false,
|
|
84
|
+
0,
|
|
85
|
+
fullReset2,
|
|
86
|
+
retryAfterMs
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
tokens -= 1;
|
|
90
|
+
await store.set(tokensKey, String(tokens), windowMs * 3);
|
|
91
|
+
await store.set(lastKey, String(now), windowMs * 3);
|
|
92
|
+
const remaining = Math.floor(tokens);
|
|
93
|
+
const fullReset = now + Math.ceil((capacity - tokens) / refillPerMs);
|
|
94
|
+
return baseResult("TOKEN_BUCKET" /* TOKEN_BUCKET */, limit, true, remaining, fullReset);
|
|
95
|
+
}
|
|
96
|
+
async function checkSlidingWindowLogLocal(store, prefix, id, limit, windowMs, now) {
|
|
97
|
+
const k = `${prefix}${id}:log`;
|
|
98
|
+
const raw = await store.get(k);
|
|
99
|
+
let entries = [];
|
|
100
|
+
if (raw) {
|
|
101
|
+
try {
|
|
102
|
+
entries = JSON.parse(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
entries = [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const cutoff = now - windowMs;
|
|
108
|
+
entries = entries.filter((t) => t > cutoff).sort((a, b) => a - b);
|
|
109
|
+
const resetMs = entries.length > 0 ? entries[0] + windowMs : now + windowMs;
|
|
110
|
+
if (entries.length >= limit) {
|
|
111
|
+
const oldest = entries[0];
|
|
112
|
+
return baseResult(
|
|
113
|
+
"SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */,
|
|
114
|
+
limit,
|
|
115
|
+
false,
|
|
116
|
+
0,
|
|
117
|
+
oldest + windowMs,
|
|
118
|
+
oldest + windowMs - now
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
entries.push(now);
|
|
122
|
+
await store.set(k, JSON.stringify(entries), windowMs * 2);
|
|
123
|
+
return baseResult("SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */, limit, true, limit - entries.length, resetMs);
|
|
124
|
+
}
|
|
125
|
+
async function checkSlidingWindowCounterLocal(store, prefix, id, limit, windowMs, now) {
|
|
126
|
+
const currStart = Math.floor(now / windowMs) * windowMs;
|
|
127
|
+
const prevStart = currStart - windowMs;
|
|
128
|
+
const kPrev = `${prefix}${id}:${prevStart}`;
|
|
129
|
+
const kCurr = `${prefix}${id}:${currStart}`;
|
|
130
|
+
const prev = parseInt(await store.get(kPrev) ?? "0", 10) || 0;
|
|
131
|
+
const curr = parseInt(await store.get(kCurr) ?? "0", 10) || 0;
|
|
132
|
+
const elapsed = now % windowMs;
|
|
133
|
+
const weight = 1 - elapsed / windowMs;
|
|
134
|
+
const estimated = Math.floor(prev * weight) + curr;
|
|
135
|
+
const resetMs = currStart + windowMs;
|
|
136
|
+
if (estimated >= limit) {
|
|
137
|
+
return baseResult(
|
|
138
|
+
"SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */,
|
|
139
|
+
limit,
|
|
140
|
+
false,
|
|
141
|
+
0,
|
|
142
|
+
resetMs,
|
|
143
|
+
resetMs - now
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
await store.set(kCurr, String(curr + 1), windowMs * 2);
|
|
147
|
+
await store.set(kPrev, String(prev), windowMs * 2);
|
|
148
|
+
const nextEstimated = estimated + 1;
|
|
149
|
+
return baseResult(
|
|
150
|
+
"SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */,
|
|
151
|
+
limit,
|
|
152
|
+
true,
|
|
153
|
+
Math.max(0, limit - nextEstimated),
|
|
154
|
+
resetMs
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/engine/redisChecks.ts
|
|
159
|
+
var import_node_crypto = require("crypto");
|
|
160
|
+
function toResult(algorithm, limit, row, now) {
|
|
161
|
+
const allowed = Number(row[0]) === 1;
|
|
162
|
+
const remaining = Number(row[1]);
|
|
163
|
+
const resetMs = Number(row[3]);
|
|
164
|
+
const retryAfterMs = row[4] != null && Number(row[4]) > 0 ? Number(row[4]) : void 0;
|
|
165
|
+
return {
|
|
166
|
+
allowed,
|
|
167
|
+
limit,
|
|
168
|
+
remaining: Math.max(0, Math.min(limit, remaining)),
|
|
169
|
+
resetMs: Number.isFinite(resetMs) ? resetMs : now,
|
|
170
|
+
retryAfterMs,
|
|
171
|
+
algorithm
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function runRedisCheck(store, prefix, id, algorithm, limit, windowMs, now) {
|
|
175
|
+
switch (algorithm) {
|
|
176
|
+
case "FIXED_WINDOW" /* FIXED_WINDOW */:
|
|
177
|
+
return fixedWindow(store, prefix, id, limit, windowMs, now);
|
|
178
|
+
case "TOKEN_BUCKET" /* TOKEN_BUCKET */:
|
|
179
|
+
return tokenBucket(store, prefix, id, limit, windowMs, now);
|
|
180
|
+
case "SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */:
|
|
181
|
+
return slidingLog(store, prefix, id, limit, windowMs, now);
|
|
182
|
+
case "SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */:
|
|
183
|
+
return slidingCounter(store, prefix, id, limit, windowMs, now);
|
|
184
|
+
default:
|
|
185
|
+
throw new Error(`Unsupported algorithm: ${String(algorithm)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function fixedWindow(store, prefix, id, limit, windowMs, now) {
|
|
189
|
+
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
190
|
+
const key = `${prefix}${id}:${windowStart}`;
|
|
191
|
+
const script = store.loadScriptFile("fixed_window.lua");
|
|
192
|
+
const raw = await store.evalScript(script, [key], [limit, windowMs, now, windowStart]);
|
|
193
|
+
return toResult("FIXED_WINDOW" /* FIXED_WINDOW */, limit, raw, now);
|
|
194
|
+
}
|
|
195
|
+
async function tokenBucket(store, prefix, id, limit, windowMs, now) {
|
|
196
|
+
const k1 = `${prefix}${id}:tokens`;
|
|
197
|
+
const k2 = `${prefix}${id}:last_refill`;
|
|
198
|
+
const script = store.loadScriptFile("token_bucket.lua");
|
|
199
|
+
const raw = await store.evalScript(script, [k1, k2], [limit, windowMs, now]);
|
|
200
|
+
return toResult("TOKEN_BUCKET" /* TOKEN_BUCKET */, limit, raw, now);
|
|
201
|
+
}
|
|
202
|
+
async function slidingLog(store, prefix, id, limit, windowMs, now) {
|
|
203
|
+
const key = `${prefix}${id}:log`;
|
|
204
|
+
const member = `${now}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
|
|
205
|
+
const script = store.loadScriptFile("sliding_window_log.lua");
|
|
206
|
+
const raw = await store.evalScript(script, [key], [limit, windowMs, now, member]);
|
|
207
|
+
return toResult("SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */, limit, raw, now);
|
|
208
|
+
}
|
|
209
|
+
async function slidingCounter(store, prefix, id, limit, windowMs, now) {
|
|
210
|
+
const currStart = Math.floor(now / windowMs) * windowMs;
|
|
211
|
+
const prevStart = currStart - windowMs;
|
|
212
|
+
const kPrev = `${prefix}${id}:${prevStart}`;
|
|
213
|
+
const kCurr = `${prefix}${id}:${currStart}`;
|
|
214
|
+
const script = store.loadScriptFile("sliding_window_counter.lua");
|
|
215
|
+
const raw = await store.evalScript(script, [kPrev, kCurr], [limit, windowMs, now]);
|
|
216
|
+
return toResult("SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */, limit, raw, now);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/store/LocalStore.ts
|
|
220
|
+
var LocalStore = class {
|
|
221
|
+
constructor(clock = () => Date.now()) {
|
|
222
|
+
this.clock = clock;
|
|
223
|
+
}
|
|
224
|
+
clock;
|
|
225
|
+
data = /* @__PURE__ */ new Map();
|
|
226
|
+
async get(key) {
|
|
227
|
+
const e = this.data.get(key);
|
|
228
|
+
if (!e) return null;
|
|
229
|
+
const now = this.clock();
|
|
230
|
+
if (e.expiresAt > 0 && now > e.expiresAt) {
|
|
231
|
+
this.data.delete(key);
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
return e.value;
|
|
235
|
+
}
|
|
236
|
+
async set(key, value, ttlMs) {
|
|
237
|
+
const expiresAt = ttlMs > 0 ? this.clock() + ttlMs : 0;
|
|
238
|
+
this.data.set(key, { value, expiresAt });
|
|
239
|
+
}
|
|
240
|
+
async del(key) {
|
|
241
|
+
this.data.delete(key);
|
|
242
|
+
}
|
|
243
|
+
async evalScript() {
|
|
244
|
+
throw new Error("LocalStore does not support evalScript \u2014 use algorithm local path");
|
|
245
|
+
}
|
|
246
|
+
/** Test helper: clear all keys */
|
|
247
|
+
_clear() {
|
|
248
|
+
this.data.clear();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/store/RedisStore.ts
|
|
253
|
+
var import_node_fs = require("fs");
|
|
254
|
+
var import_node_path = require("path");
|
|
255
|
+
var import_node_url = require("url");
|
|
256
|
+
var import_ioredis = require("ioredis");
|
|
257
|
+
var import_meta = {};
|
|
258
|
+
var __dirname = (0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
259
|
+
function scriptPath(name) {
|
|
260
|
+
const fromDist = (0, import_node_path.join)(__dirname, "..", "scripts", name);
|
|
261
|
+
const fromSourceTree = (0, import_node_path.join)(__dirname, "..", "..", "scripts", name);
|
|
262
|
+
if ((0, import_node_fs.existsSync)(fromDist)) return fromDist;
|
|
263
|
+
if ((0, import_node_fs.existsSync)(fromSourceTree)) return fromSourceTree;
|
|
264
|
+
throw new Error(
|
|
265
|
+
`FluxGuard: Lua script not found: ${name} (tried ${fromDist} and ${fromSourceTree})`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
var RedisStore = class {
|
|
269
|
+
redis;
|
|
270
|
+
owns;
|
|
271
|
+
shaCache = /* @__PURE__ */ new Map();
|
|
272
|
+
constructor(redis, optionsIsObject) {
|
|
273
|
+
if (optionsIsObject) {
|
|
274
|
+
this.redis = new import_ioredis.Redis(redis);
|
|
275
|
+
this.owns = true;
|
|
276
|
+
} else {
|
|
277
|
+
this.redis = redis;
|
|
278
|
+
this.owns = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
loadScriptFile(filename) {
|
|
282
|
+
return (0, import_node_fs.readFileSync)(scriptPath(filename), "utf8");
|
|
283
|
+
}
|
|
284
|
+
disconnect() {
|
|
285
|
+
if (this.owns) {
|
|
286
|
+
void this.redis.quit();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
get redisClient() {
|
|
290
|
+
return this.redis;
|
|
291
|
+
}
|
|
292
|
+
async get(key) {
|
|
293
|
+
const v = await this.redis.get(key);
|
|
294
|
+
return v;
|
|
295
|
+
}
|
|
296
|
+
async set(key, value, ttlMs) {
|
|
297
|
+
if (ttlMs > 0) {
|
|
298
|
+
await this.redis.set(key, value, "PX", ttlMs);
|
|
299
|
+
} else {
|
|
300
|
+
await this.redis.set(key, value);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
async del(key) {
|
|
304
|
+
await this.redis.del(key);
|
|
305
|
+
}
|
|
306
|
+
async evalScript(script, keys, args) {
|
|
307
|
+
let sha = this.shaCache.get(script);
|
|
308
|
+
if (!sha) {
|
|
309
|
+
sha = await this.redis.script("LOAD", script);
|
|
310
|
+
this.shaCache.set(script, sha);
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const r = await this.redis.evalsha(
|
|
314
|
+
sha,
|
|
315
|
+
keys.length,
|
|
316
|
+
...keys,
|
|
317
|
+
...args.map((a) => String(a))
|
|
318
|
+
);
|
|
319
|
+
return normalizeEvalResult(r);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
322
|
+
if (msg.includes("NOSCRIPT")) {
|
|
323
|
+
sha = await this.redis.script("LOAD", script);
|
|
324
|
+
this.shaCache.set(script, sha);
|
|
325
|
+
const r = await this.redis.evalsha(
|
|
326
|
+
sha,
|
|
327
|
+
keys.length,
|
|
328
|
+
...keys,
|
|
329
|
+
...args.map((a) => String(a))
|
|
330
|
+
);
|
|
331
|
+
return normalizeEvalResult(r);
|
|
332
|
+
}
|
|
333
|
+
throw e;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function normalizeEvalResult(r) {
|
|
338
|
+
if (Array.isArray(r)) {
|
|
339
|
+
return r;
|
|
340
|
+
}
|
|
341
|
+
if (r == null) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return [r];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/FluxGuard.ts
|
|
348
|
+
var FluxGuard = class {
|
|
349
|
+
constructor(cfg) {
|
|
350
|
+
this.cfg = cfg;
|
|
351
|
+
this.config = cfg;
|
|
352
|
+
this.prefix = cfg.keyPrefix ?? "fluxguard:";
|
|
353
|
+
this.nowFn = cfg.nowFn ?? (() => Date.now());
|
|
354
|
+
if (cfg.redis) {
|
|
355
|
+
const r = cfg.redis;
|
|
356
|
+
const isClient = "connect" in r && typeof r.connect === "function";
|
|
357
|
+
this.store = new RedisStore(cfg.redis, !isClient);
|
|
358
|
+
} else {
|
|
359
|
+
this.store = new LocalStore(this.nowFn);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
cfg;
|
|
363
|
+
store;
|
|
364
|
+
prefix;
|
|
365
|
+
nowFn;
|
|
366
|
+
config;
|
|
367
|
+
metrics = {
|
|
368
|
+
totalChecks: 0,
|
|
369
|
+
allowed: 0,
|
|
370
|
+
throttled: 0,
|
|
371
|
+
redisErrors: 0
|
|
372
|
+
};
|
|
373
|
+
getMetrics() {
|
|
374
|
+
return { ...this.metrics };
|
|
375
|
+
}
|
|
376
|
+
/** Merge event recording for external metrics (e.g. Prometheus). */
|
|
377
|
+
onMetricsRecord(fn) {
|
|
378
|
+
const prev = this.config.metrics?.record;
|
|
379
|
+
this.config.metrics = {
|
|
380
|
+
...this.config.metrics,
|
|
381
|
+
record: (s) => {
|
|
382
|
+
prev?.(s);
|
|
383
|
+
fn(s);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
async check(key) {
|
|
388
|
+
this.metrics.totalChecks += 1;
|
|
389
|
+
try {
|
|
390
|
+
let result;
|
|
391
|
+
if (this.store instanceof RedisStore) {
|
|
392
|
+
result = await runRedisCheck(
|
|
393
|
+
this.store,
|
|
394
|
+
this.prefix,
|
|
395
|
+
key,
|
|
396
|
+
this.cfg.algorithm,
|
|
397
|
+
this.cfg.limit,
|
|
398
|
+
this.cfg.windowMs,
|
|
399
|
+
this.nowFn()
|
|
400
|
+
);
|
|
401
|
+
} else {
|
|
402
|
+
result = await this.checkLocal(key);
|
|
403
|
+
}
|
|
404
|
+
if (result.allowed) {
|
|
405
|
+
this.metrics.allowed += 1;
|
|
406
|
+
this.config.events?.onAllowed?.(key, result);
|
|
407
|
+
} else {
|
|
408
|
+
this.metrics.throttled += 1;
|
|
409
|
+
this.config.events?.onThrottled?.(key, result);
|
|
410
|
+
}
|
|
411
|
+
this.config.metrics?.record?.(this.getMetrics());
|
|
412
|
+
return result;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (this.store instanceof RedisStore) {
|
|
415
|
+
this.metrics.redisErrors += 1;
|
|
416
|
+
this.config.events?.onRedisError?.(err);
|
|
417
|
+
if (this.cfg.failOpen !== false) {
|
|
418
|
+
const now = this.nowFn();
|
|
419
|
+
const allowed = {
|
|
420
|
+
allowed: true,
|
|
421
|
+
limit: this.cfg.limit,
|
|
422
|
+
remaining: this.cfg.limit,
|
|
423
|
+
resetMs: now + this.cfg.windowMs,
|
|
424
|
+
algorithm: this.cfg.algorithm
|
|
425
|
+
};
|
|
426
|
+
this.metrics.allowed += 1;
|
|
427
|
+
this.config.metrics?.record?.(this.getMetrics());
|
|
428
|
+
return allowed;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async checkLocal(key) {
|
|
435
|
+
const now = this.nowFn();
|
|
436
|
+
const { algorithm, limit, windowMs } = this.cfg;
|
|
437
|
+
switch (algorithm) {
|
|
438
|
+
case "FIXED_WINDOW" /* FIXED_WINDOW */:
|
|
439
|
+
return checkFixedWindowLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
440
|
+
case "TOKEN_BUCKET" /* TOKEN_BUCKET */:
|
|
441
|
+
return checkTokenBucketLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
442
|
+
case "SLIDING_WINDOW_LOG" /* SLIDING_WINDOW_LOG */:
|
|
443
|
+
return checkSlidingWindowLogLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
444
|
+
case "SLIDING_WINDOW_COUNTER" /* SLIDING_WINDOW_COUNTER */:
|
|
445
|
+
return checkSlidingWindowCounterLocal(this.store, this.prefix, key, limit, windowMs, now);
|
|
446
|
+
default:
|
|
447
|
+
throw new Error(`Unsupported algorithm: ${String(algorithm)}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// src/middleware/express.ts
|
|
453
|
+
var defaultKey = (req) => {
|
|
454
|
+
const xf = req.headers["x-forwarded-for"];
|
|
455
|
+
const ip = (typeof xf === "string" ? xf.split(",")[0]?.trim() : void 0) ?? req.socket.remoteAddress ?? "unknown";
|
|
456
|
+
return ip;
|
|
457
|
+
};
|
|
458
|
+
function fluxGuardMiddleware(limiter, options = {}) {
|
|
459
|
+
const keyExtract = options.keyExtract ?? defaultKey;
|
|
460
|
+
const sendHeaders = options.headers !== false;
|
|
461
|
+
return async (req, res, next) => {
|
|
462
|
+
const key = keyExtract(req);
|
|
463
|
+
let result;
|
|
464
|
+
try {
|
|
465
|
+
result = await limiter.check(key);
|
|
466
|
+
} catch (e) {
|
|
467
|
+
next(e);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (sendHeaders) {
|
|
471
|
+
res.setHeader("X-RateLimit-Limit", String(result.limit));
|
|
472
|
+
res.setHeader("X-RateLimit-Remaining", String(result.remaining));
|
|
473
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil(result.resetMs / 1e3)));
|
|
474
|
+
if (!result.allowed && result.retryAfterMs != null) {
|
|
475
|
+
res.setHeader("Retry-After", String(Math.ceil(result.retryAfterMs / 1e3)));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!result.allowed) {
|
|
479
|
+
if (options.onThrottled) {
|
|
480
|
+
options.onThrottled(req, res, result);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
res.status(429).json({
|
|
484
|
+
error: "Too Many Requests",
|
|
485
|
+
limit: result.limit,
|
|
486
|
+
remaining: result.remaining,
|
|
487
|
+
resetMs: result.resetMs,
|
|
488
|
+
retryAfterMs: result.retryAfterMs
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
void options.skipSuccessfulRequests;
|
|
493
|
+
void options.skipFailedRequests;
|
|
494
|
+
next();
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/metrics/prometheus.ts
|
|
499
|
+
var import_prom_client = require("prom-client");
|
|
500
|
+
function createFluxGuardPrometheusMetrics(limiter, options = {}) {
|
|
501
|
+
const prefix = options.prefix ?? "fluxguard_";
|
|
502
|
+
const registry = new import_prom_client.Registry();
|
|
503
|
+
if (options.collectDefault !== false) {
|
|
504
|
+
(0, import_prom_client.collectDefaultMetrics)({ register: registry });
|
|
505
|
+
}
|
|
506
|
+
const checksTotal = new import_prom_client.Counter({
|
|
507
|
+
name: `${prefix}checks_total`,
|
|
508
|
+
help: "Total rate limit checks",
|
|
509
|
+
registers: [registry]
|
|
510
|
+
});
|
|
511
|
+
const allowedTotal = new import_prom_client.Counter({
|
|
512
|
+
name: `${prefix}allowed_total`,
|
|
513
|
+
help: "Allowed requests",
|
|
514
|
+
registers: [registry]
|
|
515
|
+
});
|
|
516
|
+
const throttledTotal = new import_prom_client.Counter({
|
|
517
|
+
name: `${prefix}throttled_total`,
|
|
518
|
+
help: "Throttled requests",
|
|
519
|
+
registers: [registry]
|
|
520
|
+
});
|
|
521
|
+
const redisErrorsTotal = new import_prom_client.Counter({
|
|
522
|
+
name: `${prefix}redis_errors_total`,
|
|
523
|
+
help: "Redis errors in rate limiter",
|
|
524
|
+
registers: [registry]
|
|
525
|
+
});
|
|
526
|
+
let last = limiter.getMetrics();
|
|
527
|
+
limiter.onMetricsRecord((s) => {
|
|
528
|
+
const dCheck = s.totalChecks - last.totalChecks;
|
|
529
|
+
const dAllowed = s.allowed - last.allowed;
|
|
530
|
+
const dThrottled = s.throttled - last.throttled;
|
|
531
|
+
const dErr = s.redisErrors - last.redisErrors;
|
|
532
|
+
last = { ...s };
|
|
533
|
+
if (dCheck > 0) checksTotal.inc(dCheck);
|
|
534
|
+
if (dAllowed > 0) allowedTotal.inc(dAllowed);
|
|
535
|
+
if (dThrottled > 0) throttledTotal.inc(dThrottled);
|
|
536
|
+
if (dErr > 0) redisErrorsTotal.inc(dErr);
|
|
537
|
+
});
|
|
538
|
+
return {
|
|
539
|
+
registry,
|
|
540
|
+
checksTotal,
|
|
541
|
+
allowedTotal,
|
|
542
|
+
throttledTotal,
|
|
543
|
+
redisErrorsTotal
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
547
|
+
0 && (module.exports = {
|
|
548
|
+
Algorithm,
|
|
549
|
+
FluxGuard,
|
|
550
|
+
LocalStore,
|
|
551
|
+
RedisStore,
|
|
552
|
+
createFluxGuardPrometheusMetrics,
|
|
553
|
+
fluxGuardMiddleware
|
|
554
|
+
});
|
|
555
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../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":["export { FluxGuard } from \"./FluxGuard.js\";\nexport { Algorithm, type FluxGuardConfig, type RateLimitResult } from \"./types.js\";\nexport type { Store } from \"./store/Store.js\";\nexport { LocalStore } from \"./store/LocalStore.js\";\nexport { RedisStore } from \"./store/RedisStore.js\";\nexport { fluxGuardMiddleware, type ExpressLimiterOptions } from \"./middleware/express.js\";\nexport { createFluxGuardPrometheusMetrics, type PrometheusMetrics } from \"./metrics/prometheus.js\";\n","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","PRegistry","PCounter"]}
|