@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequark/quark-core",
3
- "version": "2.6.4",
3
+ "version": "2.7.0",
4
4
  "description": "Core utilities for Quark apps: auth, BullMQ queues, Redis, email, storage, rate limiting, logging, and error handling",
5
5
  "keywords": [
6
6
  "quark",
@@ -99,7 +99,13 @@ class MemoryRateLimiter {
99
99
  class RedisRateLimiter {
100
100
  constructor(redisClient, options = {}) {
101
101
  this.redis = redisClient;
102
- this.failOpen = options.failOpen ?? true;
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
  /**
@@ -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
  });