@depup/rate-limiter-flexible 9.1.1-depup.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.
Files changed (42) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +25 -0
  3. package/changes.json +5 -0
  4. package/index.js +55 -0
  5. package/lib/BurstyRateLimiter.js +78 -0
  6. package/lib/ExpressBruteFlexible.js +359 -0
  7. package/lib/RLWrapperBlackAndWhite.js +195 -0
  8. package/lib/RLWrapperTimeouts.js +82 -0
  9. package/lib/RateLimiterAbstract.js +125 -0
  10. package/lib/RateLimiterCluster.js +367 -0
  11. package/lib/RateLimiterDrizzle.js +174 -0
  12. package/lib/RateLimiterDrizzleNonAtomic.js +175 -0
  13. package/lib/RateLimiterDynamo.js +401 -0
  14. package/lib/RateLimiterEtcd.js +63 -0
  15. package/lib/RateLimiterEtcdNonAtomic.js +80 -0
  16. package/lib/RateLimiterInsuredAbstract.js +112 -0
  17. package/lib/RateLimiterMemcache.js +150 -0
  18. package/lib/RateLimiterMemory.js +106 -0
  19. package/lib/RateLimiterMongo.js +261 -0
  20. package/lib/RateLimiterMySQL.js +400 -0
  21. package/lib/RateLimiterPostgres.js +351 -0
  22. package/lib/RateLimiterPrisma.js +127 -0
  23. package/lib/RateLimiterQueue.js +131 -0
  24. package/lib/RateLimiterRedis.js +209 -0
  25. package/lib/RateLimiterRedisNonAtomic.js +195 -0
  26. package/lib/RateLimiterRes.js +64 -0
  27. package/lib/RateLimiterSQLite.js +338 -0
  28. package/lib/RateLimiterStoreAbstract.js +349 -0
  29. package/lib/RateLimiterUnion.js +51 -0
  30. package/lib/RateLimiterValkey.js +117 -0
  31. package/lib/RateLimiterValkeyGlide.js +273 -0
  32. package/lib/component/BlockedKeys/BlockedKeys.js +75 -0
  33. package/lib/component/BlockedKeys/index.js +3 -0
  34. package/lib/component/MemoryStorage/MemoryStorage.js +83 -0
  35. package/lib/component/MemoryStorage/Record.js +40 -0
  36. package/lib/component/MemoryStorage/index.js +3 -0
  37. package/lib/component/RateLimiterEtcdTransactionFailedError.js +10 -0
  38. package/lib/component/RateLimiterQueueError.js +13 -0
  39. package/lib/component/RateLimiterSetupError.js +10 -0
  40. package/lib/constants.js +21 -0
  41. package/package.json +100 -0
  42. package/types.d.ts +581 -0
@@ -0,0 +1,351 @@
1
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
2
+ const RateLimiterRes = require('./RateLimiterRes');
3
+
4
+ class RateLimiterPostgres 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: postgresClient,
15
+ * storeType: 'knex', // required only for Knex instance
16
+ * tableName: 'string',
17
+ * schemaName: 'string', // optional
18
+ * }
19
+ */
20
+ constructor(opts, cb = null) {
21
+ super(opts);
22
+
23
+ this.client = opts.storeClient;
24
+ this.clientType = opts.storeType;
25
+
26
+ this.tableName = opts.tableName;
27
+ this.schemaName = opts.schemaName;
28
+
29
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout;
30
+
31
+ this.tableCreated = opts.tableCreated;
32
+ if (!this.tableCreated) {
33
+ this._createTable()
34
+ .then(() => {
35
+ this.tableCreated = true;
36
+ if (this.clearExpiredByTimeout) {
37
+ this._clearExpiredHourAgo();
38
+ }
39
+ if (typeof cb === 'function') {
40
+ cb();
41
+ }
42
+ })
43
+ .catch((err) => {
44
+ if (typeof cb === 'function') {
45
+ cb(err);
46
+ } else {
47
+ throw err;
48
+ }
49
+ });
50
+ } else {
51
+ if (this.clearExpiredByTimeout) {
52
+ this._clearExpiredHourAgo();
53
+ }
54
+ if (typeof cb === 'function') {
55
+ cb();
56
+ }
57
+ }
58
+ }
59
+
60
+ _getTableIdentifier() {
61
+ return this.schemaName ? `"${this.schemaName}"."${this.tableName}"` : `"${this.tableName}"`;
62
+ }
63
+
64
+ clearExpired(expire) {
65
+ return new Promise((resolve) => {
66
+ const q = {
67
+ name: 'rlflx-clear-expired',
68
+ text: `DELETE FROM ${this._getTableIdentifier()} WHERE expire < $1`,
69
+ values: [expire],
70
+ };
71
+ this._query(q)
72
+ .then(() => {
73
+ resolve();
74
+ })
75
+ .catch(() => {
76
+ // Deleting expired query is not critical
77
+ resolve();
78
+ });
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Delete all rows expired 1 hour ago once per 5 minutes
84
+ *
85
+ * @private
86
+ */
87
+ _clearExpiredHourAgo() {
88
+ if (this._clearExpiredTimeoutId) {
89
+ clearTimeout(this._clearExpiredTimeoutId);
90
+ }
91
+ this._clearExpiredTimeoutId = setTimeout(() => {
92
+ this.clearExpired(Date.now() - 3600000) // Never rejected
93
+ .then(() => {
94
+ this._clearExpiredHourAgo();
95
+ });
96
+ }, 300000);
97
+ this._clearExpiredTimeoutId.unref();
98
+ }
99
+
100
+ /**
101
+ *
102
+ * @return Promise<any>
103
+ * @private
104
+ */
105
+ _getConnection() {
106
+ switch (this.clientType) {
107
+ case 'pool':
108
+ return Promise.resolve(this.client);
109
+ case 'sequelize':
110
+ return this._getSequelizeConnectionManager().getConnection();
111
+ case 'knex':
112
+ return this.client.client.acquireConnection();
113
+ case 'typeorm':
114
+ return Promise.resolve(this.client.driver.master);
115
+ default:
116
+ return Promise.resolve(this.client);
117
+ }
118
+ }
119
+
120
+ _releaseConnection(conn) {
121
+ switch (this.clientType) {
122
+ case 'pool':
123
+ return true;
124
+ case 'sequelize':
125
+ return this._getSequelizeConnectionManager().releaseConnection(conn);
126
+ case 'knex':
127
+ return this.client.client.releaseConnection(conn);
128
+ case 'typeorm':
129
+ return true;
130
+ default:
131
+ return true;
132
+ }
133
+ }
134
+
135
+ _getSequelizeConnectionManager() {
136
+ let connectionManager;
137
+ let accessError;
138
+ try {
139
+ connectionManager = this.client.connectionManager;
140
+ } catch (err) {
141
+ // Accessing connectionManager can throw in Sequelize version 7 and higher.
142
+ accessError = err;
143
+ }
144
+ if (connectionManager) {
145
+ return connectionManager;
146
+ }
147
+ if (this.client.dialect && this.client.dialect.connectionManager) {
148
+ return this.client.dialect.connectionManager;
149
+ }
150
+ // Rethrow the original error if it exists, otherwise throw a generic error
151
+ if (accessError) {
152
+ throw accessError;
153
+ }
154
+ throw new Error('Sequelize connection manager is not available');
155
+ }
156
+
157
+ /**
158
+ *
159
+ * @returns {Promise<any>}
160
+ * @private
161
+ */
162
+ _createTable() {
163
+ return new Promise((resolve, reject) => {
164
+ this._query({
165
+ text: this._getCreateTableStmt(),
166
+ })
167
+ .then(() => {
168
+ resolve();
169
+ })
170
+ .catch((err) => {
171
+ if (err.code === '23505') {
172
+ // Error: duplicate key value violates unique constraint "pg_type_typname_nsp_index"
173
+ // Postgres doesn't handle concurrent table creation
174
+ // It is supposed, that table is created by another worker
175
+ resolve();
176
+ } else {
177
+ reject(err);
178
+ }
179
+ });
180
+ });
181
+ }
182
+
183
+ _getCreateTableStmt() {
184
+ return `CREATE TABLE IF NOT EXISTS ${this._getTableIdentifier()} (
185
+ key varchar(255) PRIMARY KEY,
186
+ points integer NOT NULL DEFAULT 0,
187
+ expire bigint
188
+ );`;
189
+ }
190
+
191
+ get clientType() {
192
+ return this._clientType;
193
+ }
194
+
195
+ set clientType(value) {
196
+ const constructorName = this.client.constructor.name;
197
+
198
+ if (typeof value === 'undefined') {
199
+ if (constructorName === 'Client') {
200
+ value = 'client';
201
+ } else if (
202
+ constructorName === 'Pool' ||
203
+ constructorName === 'BoundPool'
204
+ ) {
205
+ value = 'pool';
206
+ } else if (constructorName === 'Sequelize') {
207
+ value = 'sequelize';
208
+ } else {
209
+ throw new Error('storeType is not defined');
210
+ }
211
+ }
212
+
213
+ this._clientType = value.toLowerCase();
214
+ }
215
+
216
+ get tableName() {
217
+ return this._tableName;
218
+ }
219
+
220
+ set tableName(value) {
221
+ this._tableName = typeof value === 'undefined' ? this.keyPrefix : value;
222
+ }
223
+
224
+ get schemaName() {
225
+ return this._schemaName;
226
+ }
227
+
228
+ set schemaName(value) {
229
+ this._schemaName = value;
230
+ }
231
+
232
+ get tableCreated() {
233
+ return this._tableCreated;
234
+ }
235
+
236
+ set tableCreated(value) {
237
+ this._tableCreated = typeof value === 'undefined' ? false : !!value;
238
+ }
239
+
240
+ get clearExpiredByTimeout() {
241
+ return this._clearExpiredByTimeout;
242
+ }
243
+
244
+ set clearExpiredByTimeout(value) {
245
+ this._clearExpiredByTimeout = typeof value === 'undefined' ? true : Boolean(value);
246
+ }
247
+
248
+ _getRateLimiterRes(rlKey, changedPoints, result) {
249
+ const res = new RateLimiterRes();
250
+ const row = result.rows[0];
251
+
252
+ res.isFirstInDuration = changedPoints === row.points;
253
+ res.consumedPoints = res.isFirstInDuration ? changedPoints : row.points;
254
+
255
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
256
+ res.msBeforeNext = row.expire
257
+ ? Math.max(row.expire - Date.now(), 0)
258
+ : -1;
259
+
260
+ return res;
261
+ }
262
+
263
+ _query(q) {
264
+ const prefix = this.tableName.toLowerCase();
265
+ const queryObj = { name: `${prefix}:${q.name}`, text: q.text, values: q.values };
266
+ return new Promise((resolve, reject) => {
267
+ this._getConnection()
268
+ .then((conn) => {
269
+ conn.query(queryObj)
270
+ .then((res) => {
271
+ resolve(res);
272
+ this._releaseConnection(conn);
273
+ })
274
+ .catch((err) => {
275
+ reject(err);
276
+ this._releaseConnection(conn);
277
+ });
278
+ })
279
+ .catch((err) => {
280
+ reject(err);
281
+ });
282
+ });
283
+ }
284
+
285
+ _upsert(key, points, msDuration, forceExpire = false) {
286
+ if (!this.tableCreated) {
287
+ return Promise.reject(Error('Table is not created yet'));
288
+ }
289
+
290
+ const newExpire = msDuration > 0 ? Date.now() + msDuration : null;
291
+ const expireQ = forceExpire
292
+ ? ' $3 '
293
+ : ` CASE
294
+ WHEN ${this._getTableIdentifier()}.expire <= $4 THEN $3
295
+ ELSE ${this._getTableIdentifier()}.expire
296
+ END `;
297
+
298
+ return this._query({
299
+ name: forceExpire ? 'rlflx-upsert-force' : 'rlflx-upsert',
300
+ text: `
301
+ INSERT INTO ${this._getTableIdentifier()} VALUES ($1, $2, $3)
302
+ ON CONFLICT(key) DO UPDATE SET
303
+ points = CASE
304
+ WHEN (${this._getTableIdentifier()}.expire <= $4 OR 1=${forceExpire ? 1 : 0}) THEN $2
305
+ ELSE ${this._getTableIdentifier()}.points + ($2)
306
+ END,
307
+ expire = ${expireQ}
308
+ RETURNING points, expire;`,
309
+ values: [key, points, newExpire, Date.now()],
310
+ });
311
+ }
312
+
313
+ _get(rlKey) {
314
+ if (!this.tableCreated) {
315
+ return Promise.reject(Error('Table is not created yet'));
316
+ }
317
+
318
+ return new Promise((resolve, reject) => {
319
+ this._query({
320
+ name: 'rlflx-get',
321
+ text: `
322
+ SELECT points, expire FROM ${this._getTableIdentifier()} WHERE key = $1 AND (expire > $2 OR expire IS NULL);`,
323
+ values: [rlKey, Date.now()],
324
+ })
325
+ .then((res) => {
326
+ if (res.rowCount === 0) {
327
+ res = null;
328
+ }
329
+ resolve(res);
330
+ })
331
+ .catch((err) => {
332
+ reject(err);
333
+ });
334
+ });
335
+ }
336
+
337
+ _delete(rlKey) {
338
+ if (!this.tableCreated) {
339
+ return Promise.reject(Error('Table is not created yet'));
340
+ }
341
+
342
+ return this._query({
343
+ name: 'rlflx-delete',
344
+ text: `DELETE FROM ${this._getTableIdentifier()} WHERE key = $1`,
345
+ values: [rlKey],
346
+ })
347
+ .then(res => res.rowCount > 0);
348
+ }
349
+ }
350
+
351
+ module.exports = RateLimiterPostgres;
@@ -0,0 +1,127 @@
1
+ const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
2
+ const RateLimiterRes = require('./RateLimiterRes');
3
+
4
+ class RateLimiterPrisma extends RateLimiterStoreAbstract {
5
+ /**
6
+ * Constructor for the rate limiter
7
+ * @param {Object} opts - Options for the rate limiter
8
+ */
9
+ constructor(opts) {
10
+ super(opts);
11
+
12
+ this.modelName = opts.tableName || 'RateLimiterFlexible';
13
+ this.prismaClient = opts.storeClient;
14
+ this.clearExpiredByTimeout = opts.clearExpiredByTimeout || true;
15
+
16
+ if (!this.prismaClient) {
17
+ throw new Error('Prisma client is not provided');
18
+ }
19
+
20
+ if (this.clearExpiredByTimeout) {
21
+ this._clearExpiredHourAgo();
22
+ }
23
+ }
24
+
25
+ _getRateLimiterRes(rlKey, changedPoints, result) {
26
+ const res = new RateLimiterRes();
27
+
28
+ let doc = result;
29
+
30
+ res.isFirstInDuration = doc.points === changedPoints;
31
+ res.consumedPoints = doc.points;
32
+
33
+ res.remainingPoints = Math.max(this.points - res.consumedPoints, 0);
34
+ res.msBeforeNext = doc.expire !== null
35
+ ? Math.max(new Date(doc.expire).getTime() - Date.now(), 0)
36
+ : -1;
37
+
38
+ return res;
39
+ }
40
+
41
+ _upsert(key, points, msDuration, forceExpire = false) {
42
+ if (!this.prismaClient) {
43
+ return Promise.reject(new Error('Prisma client is not established'));
44
+ }
45
+
46
+ const now = new Date();
47
+ const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
48
+
49
+ return this.prismaClient.$transaction(async (prisma) => {
50
+ const existingRecord = await prisma[this.modelName].findFirst({
51
+ where: { key: key },
52
+ });
53
+
54
+ if (existingRecord) {
55
+ // Determine if we should update the expire field
56
+ const shouldUpdateExpire = forceExpire || !existingRecord.expire || existingRecord.expire <= now || newExpire === null;
57
+
58
+ return prisma[this.modelName].update({
59
+ where: { key: key },
60
+ data: {
61
+ points: !shouldUpdateExpire ? existingRecord.points + points : points,
62
+ ...(shouldUpdateExpire && { expire: newExpire }),
63
+ },
64
+ });
65
+ } else {
66
+ return prisma[this.modelName].create({
67
+ data: {
68
+ key: key,
69
+ points: points,
70
+ expire: newExpire,
71
+ },
72
+ });
73
+ }
74
+ });
75
+ }
76
+
77
+ _get(rlKey) {
78
+ if (!this.prismaClient) {
79
+ return Promise.reject(new Error('Prisma client is not established'));
80
+ }
81
+
82
+ return this.prismaClient[this.modelName].findFirst({
83
+ where: {
84
+ AND: [
85
+ { key: rlKey },
86
+ {
87
+ OR: [
88
+ { expire: { gt: new Date() } },
89
+ { expire: null },
90
+ ],
91
+ },
92
+ ],
93
+ },
94
+ });
95
+ }
96
+
97
+ _delete(rlKey) {
98
+ if (!this.prismaClient) {
99
+ return Promise.reject(new Error('Prisma client is not established'));
100
+ }
101
+
102
+ return this.prismaClient[this.modelName].deleteMany({
103
+ where: {
104
+ key: rlKey,
105
+ },
106
+ }).then(res => res.count > 0);
107
+ }
108
+
109
+ _clearExpiredHourAgo() {
110
+ if (this._clearExpiredTimeoutId) {
111
+ clearTimeout(this._clearExpiredTimeoutId);
112
+ }
113
+ this._clearExpiredTimeoutId = setTimeout(async () => {
114
+ await this.prismaClient[this.modelName].deleteMany({
115
+ where: {
116
+ expire: {
117
+ lt: new Date(Date.now() - 3600000),
118
+ },
119
+ },
120
+ });
121
+ this._clearExpiredHourAgo();
122
+ }, 300000); // Clear every 5 minutes
123
+ this._clearExpiredTimeoutId.unref();
124
+ }
125
+ }
126
+
127
+ module.exports = RateLimiterPrisma;
@@ -0,0 +1,131 @@
1
+ const RateLimiterQueueError = require('./component/RateLimiterQueueError')
2
+ const MAX_QUEUE_SIZE = 4294967295;
3
+ const KEY_DEFAULT = 'limiter';
4
+
5
+ module.exports = class RateLimiterQueue {
6
+ constructor(limiterFlexible, opts = {}) {
7
+ const maxQueueSize =
8
+ opts.maxQueueSize !== undefined ? opts.maxQueueSize : MAX_QUEUE_SIZE;
9
+ this._queueLimiters = {
10
+ KEY_DEFAULT: new RateLimiterQueueInternal(limiterFlexible, {
11
+ ...opts,
12
+ maxQueueSize,
13
+ key: KEY_DEFAULT
14
+ }),
15
+ };
16
+ this._limiterFlexible = limiterFlexible;
17
+ this._maxQueueSize = maxQueueSize;
18
+ }
19
+
20
+ getTokensRemaining(key = KEY_DEFAULT) {
21
+ if (this._queueLimiters[key]) {
22
+ return this._queueLimiters[key].getTokensRemaining()
23
+ } else {
24
+ return Promise.resolve(this._limiterFlexible.points)
25
+ }
26
+ }
27
+
28
+ removeTokens(tokens, key = KEY_DEFAULT) {
29
+ if (!this._queueLimiters[key]) {
30
+ this._queueLimiters[key] = new RateLimiterQueueInternal(
31
+ this._limiterFlexible, {
32
+ key,
33
+ maxQueueSize: this._maxQueueSize,
34
+ })
35
+ }
36
+
37
+ return this._queueLimiters[key].removeTokens(tokens)
38
+ }
39
+ };
40
+
41
+ class RateLimiterQueueInternal {
42
+
43
+ constructor(limiterFlexible, opts = {
44
+ maxQueueSize: MAX_QUEUE_SIZE,
45
+ key: KEY_DEFAULT,
46
+ }) {
47
+ this._key = opts.key;
48
+ this._waitTimeout = null;
49
+ this._queue = [];
50
+ this._limiterFlexible = limiterFlexible;
51
+
52
+ this._maxQueueSize = opts.maxQueueSize
53
+ }
54
+
55
+ getTokensRemaining() {
56
+ return this._limiterFlexible.get(this._key)
57
+ .then((rlRes) => {
58
+ return rlRes !== null ? rlRes.remainingPoints : this._limiterFlexible.points;
59
+ })
60
+ }
61
+
62
+ removeTokens(tokens) {
63
+ const _this = this;
64
+
65
+ return new Promise((resolve, reject) => {
66
+ if (tokens > _this._limiterFlexible.points) {
67
+ reject(new RateLimiterQueueError(`Requested tokens ${tokens} exceeds maximum ${_this._limiterFlexible.points} tokens per interval`));
68
+ return
69
+ }
70
+
71
+ if (_this._queue.length > 0) {
72
+ _this._queueRequest.call(_this, resolve, reject, tokens);
73
+ } else {
74
+ _this._limiterFlexible.consume(_this._key, tokens)
75
+ .then((res) => {
76
+ resolve(res.remainingPoints);
77
+ })
78
+ .catch((rej) => {
79
+ if (rej instanceof Error) {
80
+ reject(rej);
81
+ } else {
82
+ _this._queueRequest.call(_this, resolve, reject, tokens);
83
+ if (_this._waitTimeout === null) {
84
+ _this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
85
+ }
86
+ }
87
+ });
88
+ }
89
+ })
90
+ }
91
+
92
+ _queueRequest(resolve, reject, tokens) {
93
+ const _this = this;
94
+ if (_this._queue.length < _this._maxQueueSize) {
95
+ _this._queue.push({resolve, reject, tokens});
96
+ } else {
97
+ reject(new RateLimiterQueueError(`Number of requests reached it's maximum ${_this._maxQueueSize}`))
98
+ }
99
+ }
100
+
101
+ _processFIFO() {
102
+ const _this = this;
103
+
104
+ if (_this._waitTimeout !== null) {
105
+ clearTimeout(_this._waitTimeout);
106
+ _this._waitTimeout = null;
107
+ }
108
+
109
+ if (_this._queue.length === 0) {
110
+ return;
111
+ }
112
+
113
+ const item = _this._queue.shift();
114
+ _this._limiterFlexible.consume(_this._key, item.tokens)
115
+ .then((res) => {
116
+ item.resolve(res.remainingPoints);
117
+ _this._processFIFO.call(_this);
118
+ })
119
+ .catch((rej) => {
120
+ if (rej instanceof Error) {
121
+ item.reject(rej);
122
+ _this._processFIFO.call(_this);
123
+ } else {
124
+ _this._queue.unshift(item);
125
+ if (_this._waitTimeout === null) {
126
+ _this._waitTimeout = setTimeout(_this._processFIFO.bind(_this), rej.msBeforeNext);
127
+ }
128
+ }
129
+ });
130
+ }
131
+ }