rate-limiter-flexible 5.0.5 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![node version][node-image]][node-url]
4
4
  [![deno version](https://img.shields.io/badge/deno-^1.5.3-lightgrey?logo=deno)](https://github.com/denoland/deno)
5
5
 
6
- [node-image]: https://img.shields.io/badge/node.js-%3E=_16.0-green.svg?style=flat-square
6
+ [node-image]: https://img.shields.io/badge/node.js-%3E=_18.0-green.svg?style=flat-square
7
7
  [node-url]: http://nodejs.org/download/
8
8
 
9
9
  <img src="img/rlflx-logo-small.png" width="50" alt="Logo"/>
@@ -12,7 +12,7 @@
12
12
 
13
13
  **rate-limiter-flexible** counts and limits the number of actions by key and protects from DDoS and brute force attacks at any scale.
14
14
 
15
- It works with _Redis_, _Prisma_, _DynamoDB_, process _Memory_, _Cluster_ or _PM2_, _Memcached_, _MongoDB_, _MySQL_, and _PostgreSQL_.
15
+ It works with _Redis_, _Valkey_, _Prisma_, _DynamoDB_, process _Memory_, _Cluster_ or _PM2_, _Memcached_, _MongoDB_, _MySQL_, _SQLite_, and _PostgreSQL_.
16
16
 
17
17
  Memory limiter also works in the browser.
18
18
 
@@ -24,7 +24,7 @@ Memory limiter also works in the browser.
24
24
 
25
25
  **Ready for growth.** It provides a unified API for all limiters. Whenever your application grows, it is ready. Prepare your limiters in minutes.
26
26
 
27
- **Friendly.** No matter which node package you prefer: `redis` or `ioredis`, `sequelize`/`typeorm` or `knex`, `memcached`, native driver or `mongoose`. It works with all of them.
27
+ **Friendly.** No matter which node package you prefer: `redis` or `ioredis`, `iovalkey`, `sequelize`/`typeorm` or `knex`, `memcached`, native driver or `mongoose`. It works with all of them.
28
28
 
29
29
  **In-memory blocks.** Avoid extra requests to store with [inMemoryBlockOnConsumed](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#inmemoryblockonconsumed).
30
30
 
@@ -141,10 +141,12 @@ Some copy/paste examples on Wiki:
141
141
  * [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
142
142
  * [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
143
143
  * [Prisma](https://github.com/animir/node-rate-limiter-flexible/wiki/Prisma)
144
+ * [Valkey](https://github.com/animir/node-rate-limiter-flexible/wiki/Valkey)
144
145
  * [BurstyRateLimiter](https://github.com/animir/node-rate-limiter-flexible/wiki/BurstyRateLimiter) Traffic burst support
145
146
  * [Mongo](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo) (with [sharding support](https://github.com/animir/node-rate-limiter-flexible/wiki/Mongo#mongodb-sharding-options))
146
147
  * [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
147
148
  * [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
149
+ * [SQLite](https://github.com/animir/node-rate-limiter-flexible/wiki/SQLite)
148
150
  * [RateLimiterCluster](https://github.com/animir/node-rate-limiter-flexible/wiki/Cluster) ([PM2 cluster docs read here](https://github.com/animir/node-rate-limiter-flexible/wiki/PM2-cluster))
149
151
  * [Memcache](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
150
152
  * [RateLimiterUnion](https://github.com/animir/node-rate-limiter-flexible/wiki/RateLimiterUnion) Combine 2 or more limiters to act as single
@@ -186,8 +188,8 @@ See [releases](https://github.com/animir/node-rate-limiter-flexible/releases) fo
186
188
  * [storeType](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#storetype) Have to be set to `knex`, if you use it.
187
189
  * [dbName](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#dbname) Where to store points.
188
190
  * [tableName](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#tablename) Table/collection.
189
- * [tableCreated](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#tablecreated) Is table already created in MySQL or PostgreSQL.
190
- * [clearExpiredByTimeout](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#clearexpiredbytimeout) For MySQL and PostgreSQL.
191
+ * [tableCreated](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#tablecreated) Is table already created in MySQL, SQLite or PostgreSQL.
192
+ * [clearExpiredByTimeout](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#clearexpiredbytimeout) For MySQL, SQLite and PostgreSQL.
191
193
 
192
194
  Smooth out traffic peaks:
193
195
  * [execEvenly](https://github.com/animir/node-rate-limiter-flexible/wiki/Options#execevenly)
package/index.js CHANGED
@@ -12,6 +12,8 @@ const BurstyRateLimiter = require('./lib/BurstyRateLimiter');
12
12
  const RateLimiterRes = require('./lib/RateLimiterRes');
13
13
  const RateLimiterDynamo = require('./lib/RateLimiterDynamo');
14
14
  const RateLimiterPrisma = require('./lib/RateLimiterPrisma');
15
+ const RateLimiterValkey = require('./lib/RateLimiterValkey');
16
+ const RateLimiterSQLite = require('./lib/RateLimiterSQLite');
15
17
 
16
18
  module.exports = {
17
19
  RateLimiterRedis,
@@ -30,4 +32,6 @@ module.exports = {
30
32
  RateLimiterRes,
31
33
  RateLimiterDynamo,
32
34
  RateLimiterPrisma,
35
+ RateLimiterValkey,
36
+ RateLimiterSQLite,
33
37
  };
@@ -0,0 +1,207 @@
1
+ const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract");
2
+ const RateLimiterRes = require("./RateLimiterRes");
3
+
4
+ class RateLimiterSQLite extends RateLimiterStoreAbstract {
5
+ /**
6
+ * @callback callback
7
+ * @param {Object} err
8
+ *
9
+ * @param {Object} opts
10
+ * @param {callback} cb
11
+ * Defaults {
12
+ * ... see other in RateLimiterStoreAbstract
13
+ *
14
+ * storeClient: sqliteClient, // SQLite database instance
15
+ * tableName: 'string',
16
+ * }
17
+ */
18
+ constructor(opts, cb = null) {
19
+ super(opts);
20
+
21
+ this.client = opts.storeClient;
22
+ this.tableName = opts.tableName;
23
+ this.tableCreated = opts.tableCreated || false;
24
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
25
+
26
+ if (!/^[A-Za-z0-9_]*$/.test(this.tableName)) {
27
+ const err = new Error("Table name must contain only letters and numbers");
28
+ if (typeof cb === "function") {
29
+ return cb(err);
30
+ }
31
+ throw err;
32
+ }
33
+
34
+ if (!this.tableCreated) {
35
+ this._createDbAndTable()
36
+ .then(() => {
37
+ this.tableCreated = true;
38
+
39
+ if (this.clearExpiredByTimeout) {
40
+ this._clearExpiredHourAgo();
41
+ }
42
+ if (typeof cb === "function") {
43
+ cb();
44
+ }
45
+ })
46
+ .catch((err) => {
47
+ if (typeof cb === "function") {
48
+ cb(err);
49
+ } else {
50
+ throw err;
51
+ }
52
+ });
53
+ } else {
54
+ if (this.clearExpiredByTimeout) {
55
+ this._clearExpiredHourAgo();
56
+ }
57
+ if (typeof cb === "function") {
58
+ cb();
59
+ }
60
+ }
61
+ }
62
+ async _createDbAndTable() {
63
+ return new Promise((resolve, reject) => {
64
+ const createTableSQL = this._getCreateTableSQL();
65
+ this.client.run(createTableSQL, (err) => {
66
+ if (err) {
67
+ reject(err);
68
+ } else {
69
+ resolve();
70
+ }
71
+ });
72
+ });
73
+ }
74
+
75
+ _getCreateTableSQL() {
76
+ return `CREATE TABLE IF NOT EXISTS ${this.tableName} (
77
+ key TEXT PRIMARY KEY,
78
+ points INTEGER NOT NULL DEFAULT 0,
79
+ expire INTEGER
80
+ )`;
81
+ }
82
+
83
+ _clearExpiredHourAgo() {
84
+ if (this._clearExpiredTimeoutId) {
85
+ clearTimeout(this._clearExpiredTimeoutId);
86
+ }
87
+ this._clearExpiredTimeoutId = setTimeout(() => {
88
+ this.clearExpired(Date.now() - 3600000) // Never rejected
89
+ .then(() => {
90
+ this._clearExpiredHourAgo();
91
+ });
92
+ }, 300000);
93
+ this._clearExpiredTimeoutId.unref();
94
+ }
95
+
96
+ clearExpired(nowMs) {
97
+ return new Promise((resolve) => {
98
+ this.client.run(
99
+ `DELETE FROM ${this.tableName} WHERE expire < ?`,
100
+ [nowMs],
101
+ () => resolve()
102
+ );
103
+ });
104
+ }
105
+
106
+ _getRateLimiterRes(rlKey, changedPoints, result) {
107
+ const res = new RateLimiterRes();
108
+ res.isFirstInDuration = changedPoints === result.points;
109
+ res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points;
110
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
111
+ res.msBeforeNext = result.expire
112
+ ? Math.max(result.expire - Date.now(), 0)
113
+ : -1;
114
+
115
+ return res;
116
+ }
117
+
118
+ async _upsertTransaction(rlKey, points, msDuration, forceExpire) {
119
+ return new Promise((resolve, reject) => {
120
+ const dateNow = Date.now();
121
+ const newExpire = msDuration > 0 ? dateNow + msDuration : null;
122
+
123
+ const upsertQuery = forceExpire
124
+ ? `INSERT OR REPLACE INTO ${this.tableName} (key, points, expire) VALUES (?, ?, ?) RETURNING points, expire;`
125
+ : `
126
+ INSERT INTO ${this.tableName} (key, points, expire)
127
+ VALUES (?, ?, ?)
128
+ ON CONFLICT(key) DO UPDATE SET
129
+ points = CASE
130
+ WHEN expire IS NULL OR expire > ? THEN points + excluded.points
131
+ ELSE excluded.points
132
+ END,
133
+ expire = CASE
134
+ WHEN expire IS NULL OR expire > ? THEN expire
135
+ ELSE excluded.expire
136
+ END
137
+ RETURNING points, expire;
138
+ `;
139
+
140
+ const upsertParams = forceExpire
141
+ ? [rlKey, points, newExpire]
142
+ : [rlKey, points, newExpire, dateNow, dateNow];
143
+
144
+ this.client.serialize(() => {
145
+ this.client.run("SAVEPOINT rate_limiter_trx;", (savepointErr) => {
146
+ if (savepointErr) return reject(savepointErr);
147
+
148
+ this.client.get(upsertQuery, upsertParams, (queryErr, row) => {
149
+ if (queryErr) {
150
+ return this.client.run(
151
+ "ROLLBACK TO SAVEPOINT rate_limiter_trx;",
152
+ () => reject(queryErr)
153
+ );
154
+ }
155
+
156
+ this.client.run("RELEASE SAVEPOINT rate_limiter_trx;", () =>
157
+ resolve(row)
158
+ );
159
+ });
160
+ });
161
+ });
162
+ });
163
+ }
164
+ _upsert(rlKey, points, msDuration, forceExpire = false) {
165
+ if (!this.tableCreated) {
166
+ return Promise.reject(Error("Table is not created yet"));
167
+ }
168
+ return this._upsertTransaction(rlKey, points, msDuration, forceExpire);
169
+ }
170
+ _get(rlKey) {
171
+ return new Promise((resolve, reject) => {
172
+ this.client.get(
173
+ `SELECT points, expire FROM ${this.tableName} WHERE key = ? AND (expire > ? OR expire IS NULL)`,
174
+ [rlKey, Date.now()],
175
+ (err, row) => {
176
+ if (err) {
177
+ reject(err);
178
+ } else {
179
+ resolve(row || null);
180
+ }
181
+ }
182
+ );
183
+ });
184
+ }
185
+
186
+ _delete(rlKey) {
187
+ if (!this.tableCreated) {
188
+ return Promise.reject(Error("Table is not created yet"));
189
+ }
190
+
191
+ return new Promise((resolve, reject) => {
192
+ this.client.run(
193
+ `DELETE FROM ${this.tableName} WHERE key = ?`,
194
+ [rlKey],
195
+ function (err) {
196
+ if (err) {
197
+ reject(err);
198
+ } else {
199
+ resolve(this.changes > 0);
200
+ }
201
+ }
202
+ );
203
+ });
204
+ }
205
+ }
206
+
207
+ module.exports = RateLimiterSQLite;
@@ -0,0 +1,117 @@
1
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
2
+ const RateLimiterRes = require('./RateLimiterRes');
3
+
4
+ const incrTtlLuaScript = `
5
+ server.call('set', KEYS[1], 0, 'EX', ARGV[2], 'NX')
6
+ local consumed = server.call('incrby', KEYS[1], ARGV[1])
7
+ local ttl = server.call('pttl', KEYS[1])
8
+ return {consumed, ttl}
9
+ `;
10
+
11
+ class RateLimiterValkey extends RateLimiterStoreAbstract {
12
+ /**
13
+ *
14
+ * @param {Object} opts
15
+ * Defaults {
16
+ * ... see other in RateLimiterStoreAbstract
17
+ *
18
+ * storeClient: ValkeyClient
19
+ * rejectIfValkeyNotReady: boolean = false - reject / invoke insuranceLimiter immediately when valkey connection is not "ready"
20
+ * }
21
+ */
22
+ constructor(opts) {
23
+ super(opts);
24
+ this.client = opts.storeClient;
25
+
26
+ this._rejectIfValkeyNotReady = !!opts.rejectIfValkeyNotReady;
27
+ this._incrTtlLuaScript = opts.customIncrTtlLuaScript || incrTtlLuaScript;
28
+
29
+ this.client.defineCommand('rlflxIncr', {
30
+ numberOfKeys: 1,
31
+ lua: this._incrTtlLuaScript,
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Prevent actual valkey call if valkey connection is not ready
37
+ * @return {boolean}
38
+ * @private
39
+ */
40
+ _isValkeyReady() {
41
+ if (!this._rejectIfValkeyNotReady) {
42
+ return true;
43
+ }
44
+
45
+ return this.client.status === 'ready';
46
+ }
47
+
48
+ _getRateLimiterRes(rlKey, changedPoints, result) {
49
+ let consumed;
50
+ let resTtlMs;
51
+
52
+ if (Array.isArray(result[0])) {
53
+ [[, consumed], [, resTtlMs]] = result;
54
+ } else {
55
+ [consumed, resTtlMs] = result;
56
+ }
57
+
58
+ const res = new RateLimiterRes();
59
+ res.consumedPoints = +consumed;
60
+ res.isFirstInDuration = res.consumedPoints === changedPoints;
61
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
62
+ res.msBeforeNext = resTtlMs;
63
+
64
+ return res;
65
+ }
66
+
67
+ _upsert(rlKey, points, msDuration, forceExpire = false) {
68
+ if (!this._isValkeyReady()) {
69
+ throw new Error('Valkey connection is not ready');
70
+ }
71
+
72
+ const secDuration = Math.floor(msDuration / 1000);
73
+
74
+ if (forceExpire) {
75
+ const multi = this.client.multi();
76
+
77
+ if (secDuration > 0) {
78
+ multi.set(rlKey, points, 'EX', secDuration);
79
+ } else {
80
+ multi.set(rlKey, points);
81
+ }
82
+
83
+ return multi.pttl(rlKey).exec();
84
+ }
85
+
86
+ if (secDuration > 0) {
87
+ return this.client.rlflxIncr([rlKey, String(points), String(secDuration), String(this.points), String(this.duration)]);
88
+ }
89
+
90
+ return this.client.multi().incrby(rlKey, points).pttl(rlKey).exec();
91
+ }
92
+
93
+ _get(rlKey) {
94
+ if (!this._isValkeyReady()) {
95
+ throw new Error('Valkey connection is not ready');
96
+ }
97
+
98
+ return this.client
99
+ .multi()
100
+ .get(rlKey)
101
+ .pttl(rlKey)
102
+ .exec()
103
+ .then((result) => {
104
+ const [[, points]] = result;
105
+ if (points === null) return null;
106
+ return result;
107
+ });
108
+ }
109
+
110
+ _delete(rlKey) {
111
+ return this.client
112
+ .del(rlKey)
113
+ .then(result => result > 0);
114
+ }
115
+ }
116
+
117
+ module.exports = RateLimiterValkey;
package/lib/constants.js CHANGED
@@ -8,6 +8,7 @@ const LIMITER_TYPES = {
8
8
  POSTGRES: 'postgres',
9
9
  DYNAMO: 'dynamo',
10
10
  PRISMA: 'prisma',
11
+ SQLITE: 'sqlite',
11
12
  };
12
13
 
13
14
  const ERR_UNKNOWN_LIMITER_TYPE_MESSAGE = 'Unknown limiter type. Use one of LIMITER_TYPES constants.';
package/lib/index.d.ts CHANGED
@@ -258,6 +258,10 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions {
258
258
  customIncrTtlLuaScript?: string;
259
259
  }
260
260
 
261
+ interface IRateLimiterValkeyOptions extends IRateLimiterStoreOptions {
262
+ customIncrTtlLuaScript?: string;
263
+ }
264
+
261
265
  interface ICallbackReady {
262
266
  (error?: Error): void;
263
267
  }
@@ -291,6 +295,10 @@ export class RateLimiterRedis extends RateLimiterStoreAbstract {
291
295
  constructor(opts: IRateLimiterRedisOptions);
292
296
  }
293
297
 
298
+ export class RateLimiterValkey extends RateLimiterStoreAbstract {
299
+ constructor(opts: IRateLimiterValkeyOptions);
300
+ }
301
+
294
302
  export interface IRateLimiterMongoFunctionOptions {
295
303
  attrs: { [key: string]: any };
296
304
  }
@@ -350,6 +358,10 @@ export class RateLimiterPostgres extends RateLimiterStoreAbstract {
350
358
  constructor(opts: IRateLimiterPostgresOptions, cb?: ICallbackReady);
351
359
  }
352
360
 
361
+ export class RateLimiterSQLite extends RateLimiterStoreAbstract {
362
+ constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
363
+ }
364
+
353
365
  export class RateLimiterPrisma extends RateLimiterStoreAbstract {
354
366
  constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
355
367
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rate-limiter-flexible",
3
- "version": "5.0.5",
3
+ "version": "6.1.0",
4
4
  "description": "Node.js rate limiter by key and protection from DDoS and Brute-Force attacks in process Memory, Redis, MongoDb, Memcached, MySQL, PostgreSQL, Cluster or PM",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -53,6 +53,7 @@
53
53
  "eslint-plugin-node": "^6.0.1",
54
54
  "eslint-plugin-security": "^1.4.0",
55
55
  "ioredis": "^5.3.2",
56
+ "iovalkey": "^0.3.1",
56
57
  "istanbul": "^1.1.0-alpha.1",
57
58
  "memcached-mock": "^0.1.0",
58
59
  "mocha": "^10.2.0",
@@ -60,7 +61,8 @@
60
61
  "prisma": "^5.8.0",
61
62
  "redis": "^4.6.8",
62
63
  "redis-mock": "^0.48.0",
63
- "sinon": "^17.0.1"
64
+ "sinon": "^17.0.1",
65
+ "sqlite3": "^5.1.7"
64
66
  },
65
67
  "browser": {
66
68
  "cluster": false,