@usequark/quark-core 2.6.4 → 2.7.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/rate-limiter.js +7 -1
- package/src/rate-limiter.test.js +44 -0
package/package.json
CHANGED
package/src/rate-limiter.js
CHANGED
|
@@ -99,7 +99,13 @@ class MemoryRateLimiter {
|
|
|
99
99
|
class RedisRateLimiter {
|
|
100
100
|
constructor(redisClient, options = {}) {
|
|
101
101
|
this.redis = redisClient;
|
|
102
|
-
|
|
102
|
+
// Fails CLOSED by default: if Redis errors mid-request, the request is
|
|
103
|
+
// treated as rate-limited rather than allowed through. A limiter that
|
|
104
|
+
// disables itself precisely when it is being hammered (the classic
|
|
105
|
+
// fail-open) is worst exactly when it matters. Opt back in with
|
|
106
|
+
// `failOpen: true` only if an outage-driven 429 is worse for you than
|
|
107
|
+
// an attacker-driven flood.
|
|
108
|
+
this.failOpen = options.failOpen ?? false;
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/**
|
package/src/rate-limiter.test.js
CHANGED
|
@@ -127,4 +127,48 @@ test("Rate Limiter Module", async (t) => {
|
|
|
127
127
|
limiter.stopCleanup();
|
|
128
128
|
assert(limiter.cleanupInterval === null);
|
|
129
129
|
});
|
|
130
|
+
|
|
131
|
+
// A Redis-backed limiter that fails OPEN disables itself precisely when it
|
|
132
|
+
// is being hammered — Redis erroring under load is the moment an attacker
|
|
133
|
+
// most needs the limiter to hold. Default must be fail-CLOSED.
|
|
134
|
+
await t.test(
|
|
135
|
+
"Redis limiter fails CLOSED by default when Redis errors",
|
|
136
|
+
async () => {
|
|
137
|
+
const limiter = createRateLimiter({
|
|
138
|
+
type: "redis",
|
|
139
|
+
redisClient: {
|
|
140
|
+
eval: async () => {
|
|
141
|
+
throw new Error("redis down");
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const result = await limiter.checkLimit("k", 100, 60000);
|
|
147
|
+
assert.strictEqual(
|
|
148
|
+
result.limited,
|
|
149
|
+
true,
|
|
150
|
+
"must block, not allow, on error",
|
|
151
|
+
);
|
|
152
|
+
assert.strictEqual(result.remaining, 0);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await t.test(
|
|
157
|
+
"Redis limiter can opt back into fail-open explicitly",
|
|
158
|
+
async () => {
|
|
159
|
+
const limiter = createRateLimiter({
|
|
160
|
+
type: "redis",
|
|
161
|
+
failOpen: true,
|
|
162
|
+
redisClient: {
|
|
163
|
+
eval: async () => {
|
|
164
|
+
throw new Error("redis down");
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const result = await limiter.checkLimit("k", 100, 60000);
|
|
170
|
+
assert.strictEqual(result.limited, false);
|
|
171
|
+
assert.strictEqual(result.remaining, 100);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
130
174
|
});
|