rate-limiter-flexible 11.0.1 → 11.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 +1 -1
- package/lib/RateLimiterDrizzle.js +68 -15
- package/lib/RateLimiterMemory.js +90 -0
- package/lib/component/MemoryStorage/MemoryStorage.js +41 -0
- package/package.json +3 -2
- package/types.d.ts +41 -6
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ Copy/paste examples on Wiki:
|
|
|
136
136
|
* [DynamoDb](https://github.com/animir/node-rate-limiter-flexible/wiki/DynamoDB)
|
|
137
137
|
* [Etcd](https://github.com/animir/node-rate-limiter-flexible/wiki/Etcd) Atomic and non-atomic counters.
|
|
138
138
|
* [Memcached](https://github.com/animir/node-rate-limiter-flexible/wiki/Memcache)
|
|
139
|
-
* [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory)
|
|
139
|
+
* [Memory](https://github.com/animir/node-rate-limiter-flexible/wiki/Memory) Supports dump() and restore() for graceful restarts and green/blue deployment
|
|
140
140
|
* [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))
|
|
141
141
|
* [MySQL](https://github.com/animir/node-rate-limiter-flexible/wiki/MySQL) (support Sequelize and Knex)
|
|
142
142
|
* [Postgres](https://github.com/animir/node-rate-limiter-flexible/wiki/PostgreSQL) (support Sequelize, TypeORM and Knex)
|
|
@@ -9,23 +9,76 @@ class RateLimiterDrizzleError extends Error {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async function
|
|
12
|
+
async function loadDrizzleOperators() {
|
|
13
13
|
if (drizzleOperators) return drizzleOperators;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const requiredOperators = ['and', 'or', 'gt', 'lt', 'eq', 'isNull', 'sql'];
|
|
16
|
+
|
|
17
|
+
// Use dynamic import to prevent static analysis tools from detecting the import
|
|
18
|
+
function getPackageName() {
|
|
19
|
+
return ['drizzle', 'orm'].join('-');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const collectModuleLayers = (mod) => {
|
|
23
|
+
const layers = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
let current = mod;
|
|
26
|
+
|
|
27
|
+
while (current && (typeof current === 'object' || typeof current === 'function') && !seen.has(current)) {
|
|
28
|
+
layers.push(current);
|
|
29
|
+
seen.add(current);
|
|
30
|
+
current = current.default;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return layers;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const importModule = async (specifier, { optional = false } = {}) => {
|
|
37
|
+
try {
|
|
38
|
+
return await import(/* @vite-ignore */ specifier);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (optional) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
throw new RateLimiterDrizzleError(
|
|
44
|
+
error.message || 'Failed to import drizzle-orm package.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const mainModule = await importModule(`${getPackageName()}`);
|
|
50
|
+
const sqlModule = await importModule(`${getPackageName()}/sql`, { optional: true });
|
|
51
|
+
|
|
52
|
+
const mainCandidates = collectModuleLayers(mainModule);
|
|
53
|
+
const sqlCandidates = sqlModule ? collectModuleLayers(sqlModule) : [];
|
|
54
|
+
const resolvedOperators = {};
|
|
55
|
+
const missingOperators = [];
|
|
56
|
+
|
|
57
|
+
for (const operatorName of requiredOperators) {
|
|
58
|
+
const candidates = operatorName === 'sql'
|
|
59
|
+
? [...sqlCandidates, ...mainCandidates]
|
|
60
|
+
: mainCandidates;
|
|
61
|
+
|
|
62
|
+
const operator = candidates
|
|
63
|
+
.map((candidate) => candidate?.[operatorName])
|
|
64
|
+
.find((value) => typeof value === 'function');
|
|
65
|
+
|
|
66
|
+
if (!operator) {
|
|
67
|
+
missingOperators.push(operatorName);
|
|
68
|
+
continue;
|
|
19
69
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
70
|
+
|
|
71
|
+
resolvedOperators[operatorName] = operator;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (missingOperators.length > 0) {
|
|
25
75
|
throw new RateLimiterDrizzleError(
|
|
26
|
-
|
|
76
|
+
`Failed to load drizzle-orm operators: missing ${missingOperators.join(', ')}`
|
|
27
77
|
);
|
|
28
78
|
}
|
|
79
|
+
|
|
80
|
+
drizzleOperators = resolvedOperators;
|
|
81
|
+
return drizzleOperators;
|
|
29
82
|
}
|
|
30
83
|
|
|
31
84
|
const RateLimiterStoreAbstract = require('./RateLimiterStoreAbstract');
|
|
@@ -71,7 +124,7 @@ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
|
|
|
71
124
|
return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
|
|
72
125
|
}
|
|
73
126
|
|
|
74
|
-
const { eq, sql } = await
|
|
127
|
+
const { eq, sql } = await loadDrizzleOperators();
|
|
75
128
|
const now = new Date();
|
|
76
129
|
const newExpire = msDuration > 0 ? new Date(now.getTime() + msDuration) : null;
|
|
77
130
|
|
|
@@ -117,7 +170,7 @@ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
|
|
|
117
170
|
return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
|
|
118
171
|
}
|
|
119
172
|
|
|
120
|
-
const { and, or, gt, eq, isNull } = await
|
|
173
|
+
const { and, or, gt, eq, isNull } = await loadDrizzleOperators();
|
|
121
174
|
|
|
122
175
|
const [response] = await this.drizzleClient
|
|
123
176
|
.select()
|
|
@@ -139,7 +192,7 @@ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
|
|
|
139
192
|
return Promise.reject(new RateLimiterDrizzleError('Drizzle client is not established'))
|
|
140
193
|
}
|
|
141
194
|
|
|
142
|
-
const { eq } = await
|
|
195
|
+
const { eq } = await loadDrizzleOperators();
|
|
143
196
|
|
|
144
197
|
const [result] = await this.drizzleClient
|
|
145
198
|
.delete(this.schema)
|
|
@@ -156,7 +209,7 @@ class RateLimiterDrizzle extends RateLimiterStoreAbstract {
|
|
|
156
209
|
|
|
157
210
|
this._clearExpiredTimeoutId = setTimeout(async () => {
|
|
158
211
|
try {
|
|
159
|
-
const { lt } = await
|
|
212
|
+
const { lt } = await loadDrizzleOperators();
|
|
160
213
|
await this.drizzleClient
|
|
161
214
|
.delete(this.schema)
|
|
162
215
|
.where(lt(this.schema.expire, new Date(Date.now() - EXPIRED_THRESHOLD_MS)));
|
package/lib/RateLimiterMemory.js
CHANGED
|
@@ -102,6 +102,96 @@ class RateLimiterMemory extends RateLimiterAbstract {
|
|
|
102
102
|
delete(key) {
|
|
103
103
|
return Promise.resolve(this._memoryStorage.delete(this.getKey(key)));
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
dump() {
|
|
107
|
+
const storage = [];
|
|
108
|
+
for (const [key, record] of this._memoryStorage._storage) {
|
|
109
|
+
storage.push({
|
|
110
|
+
key: this.parseKey(key),
|
|
111
|
+
value: record.value,
|
|
112
|
+
expiresAt: record.expiresAt,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
version: 1,
|
|
118
|
+
dumpedAt: Date.now(),
|
|
119
|
+
storage,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Restores rate-limiter state from a previously dumped snapshot.
|
|
125
|
+
*
|
|
126
|
+
* Each entry is classified into one of three buckets:
|
|
127
|
+
* - **invalid** – the item is not an object or has a malformed schema (bad key, value or expiry type).
|
|
128
|
+
* - **expired** – the item has logically expired based on current timestamp.
|
|
129
|
+
* - **restored** – the item was valid and successfully loaded into storage.
|
|
130
|
+
*
|
|
131
|
+
* @param {Object} data - Snapshot produced by {@link dump}.
|
|
132
|
+
* @param {boolean} [detailResponse=false] - When `true`, each bucket also
|
|
133
|
+
* carries a `keys` array listing every affected key.
|
|
134
|
+
* @returns {{ invalid, expired, restored } | undefined} Summary counts (and
|
|
135
|
+
* optionally keys). Returns `undefined` when `data` fails schema checks.
|
|
136
|
+
*/
|
|
137
|
+
restore(data, detailResponse = false) {
|
|
138
|
+
if (!data || typeof data !== 'object' || data.version !== 1) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const response = detailResponse
|
|
143
|
+
? {
|
|
144
|
+
invalid: { count: 0, keys: [] },
|
|
145
|
+
expired: { count: 0, keys: [] },
|
|
146
|
+
restored: { count: 0, keys: [] },
|
|
147
|
+
}
|
|
148
|
+
: { invalid: 0, expired: 0, restored: 0 };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Increments the named bucket and, in detail mode, appends the key.
|
|
152
|
+
* @param {'invalid'|'expired'|'restored'} bucket
|
|
153
|
+
* @param {string|number} key
|
|
154
|
+
*/
|
|
155
|
+
const record = (bucket, key) => {
|
|
156
|
+
if (detailResponse) {
|
|
157
|
+
response[bucket].count += 1;
|
|
158
|
+
response[bucket].keys.push(key);
|
|
159
|
+
} else {
|
|
160
|
+
response[bucket] += 1;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (!Array.isArray(data.storage)) {
|
|
165
|
+
return response;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (const item of data.storage) {
|
|
169
|
+
if (!item || typeof item !== 'object') {
|
|
170
|
+
//in array if we don't get object we push N/A
|
|
171
|
+
record('invalid', 'N/A');
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const isValidKey = typeof item.key === 'string' || typeof item.key === 'number';
|
|
176
|
+
const isValidValue = Number.isFinite(item.value);
|
|
177
|
+
const isValidExpiry = item.expiresAt === null || Number.isFinite(item.expiresAt);
|
|
178
|
+
|
|
179
|
+
if (!isValidKey || !isValidValue || !isValidExpiry) {
|
|
180
|
+
record('invalid', item.key);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (item.expiresAt !== null && item.expiresAt <= Date.now()) {
|
|
185
|
+
record('expired', item.key);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this._memoryStorage._restoreRecord(this.getKey(item.key), item.value, item.expiresAt);
|
|
190
|
+
record('restored', item.key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return response;
|
|
194
|
+
}
|
|
105
195
|
}
|
|
106
196
|
|
|
107
197
|
module.exports = RateLimiterMemory;
|
|
@@ -86,4 +86,45 @@ module.exports = class MemoryStorage {
|
|
|
86
86
|
}
|
|
87
87
|
return false;
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @internal
|
|
92
|
+
* @param key
|
|
93
|
+
* @param value
|
|
94
|
+
* @param expiresAt
|
|
95
|
+
*/
|
|
96
|
+
_restoreRecord(key, value, expiresAt) {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
const hasExpiresAt = expiresAt !== null;
|
|
99
|
+
|
|
100
|
+
if (hasExpiresAt && !Number.isFinite(expiresAt)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (hasExpiresAt && expiresAt <= now) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const durationMs = hasExpiresAt ? expiresAt - now : 0;
|
|
109
|
+
|
|
110
|
+
const existingRecord = this._storage.get(key);
|
|
111
|
+
if (existingRecord && existingRecord.timeoutId) {
|
|
112
|
+
clearTimeout(existingRecord.timeoutId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const record = new Record(
|
|
116
|
+
value,
|
|
117
|
+
expiresAt
|
|
118
|
+
);
|
|
119
|
+
this._storage.set(key, record);
|
|
120
|
+
|
|
121
|
+
if (durationMs > 0) {
|
|
122
|
+
record.timeoutId = setTimeout(() => {
|
|
123
|
+
this._storage.delete(key);
|
|
124
|
+
}, durationMs);
|
|
125
|
+
if (record.timeoutId.unref) {
|
|
126
|
+
record.timeoutId.unref();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
89
130
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rate-limiter-flexible",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Node.js atomic and non-atomic counters, rate limiting tools, protection from DoS and brute-force attacks at scale",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"test:valkey-cluster": "VALKEY_CLUSTER_PORT=7001 mocha test/RateLimiterValkeyGlide.test.js -- -g 'RateLimiterValkeyGlide with cluster client'",
|
|
12
12
|
"prisma:postgres": "prisma generate --schema=./test/RateLimiterPrisma/Postgres/schema.prisma && prisma db push --schema=./test/RateLimiterPrisma/Postgres/schema.prisma",
|
|
13
13
|
"drizzle:postgres": "cd ./test/RateLimiterDrizzle/Postgres && drizzle-kit push",
|
|
14
|
-
"test": "npm
|
|
14
|
+
"test:drizzle:postgres:v1": "npm i --no-save --no-package-lock drizzle-orm@^1.0.0-beta.23 && mocha test/RateLimiterDrizzle/Postgres/RateLimiterDrizzlePostgres.test.js && ( [ \"$GITHUB_ACTIONS\" = \"true\" ] || npm i )",
|
|
15
|
+
"test": "npm run prisma:postgres && npm run drizzle:postgres && nyc --reporter=html --reporter=text mocha \"test/**/*.test.js\" && npm run test:drizzle:postgres:v1",
|
|
15
16
|
"debug-test": "mocha --inspect-brk lib/**/**.test.js",
|
|
16
17
|
"coveralls": "cat ./coverage/lcov.info | coveralls",
|
|
17
18
|
"eslint": "eslint --quiet lib/**/**.js test/**/**.js",
|
package/types.d.ts
CHANGED
|
@@ -318,7 +318,7 @@ interface IRateLimiterRedisOptions extends IRateLimiterStoreOptions {
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
interface IRateLimiterValkeyOptions extends IRateLimiterStoreOptions {
|
|
321
|
-
|
|
321
|
+
customIncrTtlLuaScript?: string;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
interface ICallbackReady {
|
|
@@ -334,8 +334,43 @@ interface IRLWrapperBlackAndWhiteOptions {
|
|
|
334
334
|
runActionAnyway?: boolean;
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
export interface IRateLimiterMemoryRecord {
|
|
338
|
+
key: string | number;
|
|
339
|
+
value: number;
|
|
340
|
+
expiresAt: number | null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export interface IRateLimiterMemoryExport {
|
|
344
|
+
version: number;
|
|
345
|
+
dumpedAt: number;
|
|
346
|
+
storage: IRateLimiterMemoryRecord[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Common structure for restore results. */
|
|
350
|
+
export interface IRateLimiterRestoreResult<T> {
|
|
351
|
+
invalid: T;
|
|
352
|
+
expired: T;
|
|
353
|
+
restored: T;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Returned by {@link RateLimiterMemory.restore} when `detailResponse` is `false` (default). */
|
|
357
|
+
export type IRateLimiterRestoreResponse = IRateLimiterRestoreResult<number>;
|
|
358
|
+
|
|
359
|
+
export interface IRateLimiterRestoreBucketDetail {
|
|
360
|
+
count: number;
|
|
361
|
+
keys: Array<string | number>;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Returned by {@link RateLimiterMemory.restore} when `detailResponse` is `true`. */
|
|
365
|
+
export type IRateLimiterRestoreDetailResponse = IRateLimiterRestoreResult<IRateLimiterRestoreBucketDetail>;
|
|
366
|
+
|
|
337
367
|
export class RateLimiterMemory extends RateLimiterAbstract {
|
|
338
368
|
constructor(opts: IRateLimiterOptions);
|
|
369
|
+
|
|
370
|
+
dump(): IRateLimiterMemoryExport;
|
|
371
|
+
restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse: true): IRateLimiterRestoreDetailResponse | undefined;
|
|
372
|
+
restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse?: false): IRateLimiterRestoreResponse | undefined;
|
|
373
|
+
restore(data: Omit<IRateLimiterMemoryExport, 'dumpedAt'>, detailResponse?: boolean): IRateLimiterRestoreResponse | IRateLimiterRestoreDetailResponse | undefined;
|
|
339
374
|
}
|
|
340
375
|
|
|
341
376
|
export class RateLimiterCluster extends RateLimiterAbstract {
|
|
@@ -359,7 +394,7 @@ export class RateLimiterRedisNonAtomic extends RateLimiterStoreAbstract {
|
|
|
359
394
|
}
|
|
360
395
|
|
|
361
396
|
export class RateLimiterValkey extends RateLimiterStoreAbstract {
|
|
362
|
-
|
|
397
|
+
constructor(opts: IRateLimiterValkeyOptions);
|
|
363
398
|
}
|
|
364
399
|
|
|
365
400
|
export interface IRateLimiterMongoFunctionOptions {
|
|
@@ -424,19 +459,19 @@ export class RateLimiterPostgres extends RateLimiterStoreAbstract {
|
|
|
424
459
|
}
|
|
425
460
|
|
|
426
461
|
export class RateLimiterSQLite extends RateLimiterStoreAbstract {
|
|
427
|
-
|
|
462
|
+
constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
|
|
428
463
|
}
|
|
429
464
|
|
|
430
465
|
export class RateLimiterPrisma extends RateLimiterStoreAbstract {
|
|
431
|
-
|
|
466
|
+
constructor(opts: IRateLimiterStoreNoAutoExpiryOptions, cb?: ICallbackReady);
|
|
432
467
|
}
|
|
433
468
|
|
|
434
469
|
export class RateLimiterDrizzle extends RateLimiterStoreAbstract {
|
|
435
|
-
|
|
470
|
+
constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
|
|
436
471
|
}
|
|
437
472
|
|
|
438
473
|
export class RateLimiterDrizzleNonAtomic extends RateLimiterStoreAbstract {
|
|
439
|
-
|
|
474
|
+
constructor(opts: IRateLimiterStoreNoAutoExpiryOptionsAndSchema, cb?: ICallbackReady);
|
|
440
475
|
}
|
|
441
476
|
|
|
442
477
|
export class RateLimiterMemcache extends RateLimiterStoreAbstract { }
|