@zucker-framework/rate-limiter 1.0.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/LICENSE +201 -0
- package/NOTICE +22 -0
- package/THIRD_PARTY_NOTICES +4 -0
- package/dist/index.d.mts +184 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +574 -0
- package/dist/index.mjs +542 -0
- package/package.json +40 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/rate-limiter.service.ts
|
|
5
|
+
import { Injectable, Logger, HttpException, HttpStatus } from "@nestjs/common";
|
|
6
|
+
import { RateLimiterMemory, RateLimiterRedis } from "rate-limiter-flexible";
|
|
7
|
+
import { BusinessException, NotFoundException } from "@zucker-framework/core";
|
|
8
|
+
import { createHmac } from "crypto";
|
|
9
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
10
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
12
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
13
|
+
} else {
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
if (d = decorators[i]) {
|
|
16
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
21
|
+
}
|
|
22
|
+
__name(_ts_decorate, "_ts_decorate");
|
|
23
|
+
function _ts_metadata(metadataKey, metadataValue) {
|
|
24
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
25
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
__name(_ts_metadata, "_ts_metadata");
|
|
29
|
+
function deriveRateLimitKey(key, secret) {
|
|
30
|
+
if (!secret) return key;
|
|
31
|
+
return `v1:${createHmac("sha256", secret).update(key).digest("hex")}`;
|
|
32
|
+
}
|
|
33
|
+
__name(deriveRateLimitKey, "deriveRateLimitKey");
|
|
34
|
+
var TooManyRequestsException = class extends HttpException {
|
|
35
|
+
static {
|
|
36
|
+
__name(this, "TooManyRequestsException");
|
|
37
|
+
}
|
|
38
|
+
retryAfter;
|
|
39
|
+
constructor(message, retryAfter) {
|
|
40
|
+
super({
|
|
41
|
+
statusCode: HttpStatus.TOO_MANY_REQUESTS,
|
|
42
|
+
message,
|
|
43
|
+
retryAfter
|
|
44
|
+
}, HttpStatus.TOO_MANY_REQUESTS);
|
|
45
|
+
this.retryAfter = retryAfter;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var RateLimiterService = class _RateLimiterService {
|
|
49
|
+
static {
|
|
50
|
+
__name(this, "RateLimiterService");
|
|
51
|
+
}
|
|
52
|
+
options;
|
|
53
|
+
logger = new Logger(_RateLimiterService.name);
|
|
54
|
+
limiters = /* @__PURE__ */ new Map();
|
|
55
|
+
useRedis = false;
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.options = options;
|
|
58
|
+
}
|
|
59
|
+
async onModuleInit() {
|
|
60
|
+
if (this.options.redisClient) {
|
|
61
|
+
this.useRedis = true;
|
|
62
|
+
this.logger.log("RateLimiterService \u4F7F\u7528 Redis \u5206\u5E03\u5F0F\u9650\u6D41");
|
|
63
|
+
}
|
|
64
|
+
for (const [name, config] of Object.entries(this.options.limiters)) {
|
|
65
|
+
if (config.points <= 0 || config.duration <= 0) {
|
|
66
|
+
this.logger.warn(`\u9650\u6D41\u5668 '${name}' \u914D\u7F6E\u65E0\u6548 (points=${config.points}, duration=${config.duration})\uFF0C\u5DF2\u8DF3\u8FC7`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const opts = {
|
|
70
|
+
points: config.points,
|
|
71
|
+
duration: config.duration,
|
|
72
|
+
blockDuration: config.blockDuration ?? config.duration
|
|
73
|
+
};
|
|
74
|
+
if (this.useRedis) {
|
|
75
|
+
this.limiters.set(name, new RateLimiterRedis({
|
|
76
|
+
...opts,
|
|
77
|
+
storeClient: this.options.redisClient,
|
|
78
|
+
keyPrefix: config.keyPrefix ?? `rl:${name}:`
|
|
79
|
+
}));
|
|
80
|
+
} else {
|
|
81
|
+
this.limiters.set(name, new RateLimiterMemory(opts));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.logger.log(`RateLimiterService \u521D\u59CB\u5316\u5B8C\u6210 (${this.useRedis ? "Redis" : "Memory"}), ${this.limiters.size} \u4E2A\u9650\u6D41\u5668`);
|
|
85
|
+
}
|
|
86
|
+
onModuleDestroy() {
|
|
87
|
+
this.limiters.clear();
|
|
88
|
+
this.logger.log("RateLimiterService \u5DF2\u6E05\u7406");
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Consume points from a rate limiter.
|
|
92
|
+
* Throws TooManyRequestsException (429) when limit is exceeded.
|
|
93
|
+
*/
|
|
94
|
+
async consume(limiterName, key, pointsToConsume = 1, errorMessage = "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5") {
|
|
95
|
+
const limiter = this.getLimiter(limiterName);
|
|
96
|
+
const storageKey = this.toStorageKey(key);
|
|
97
|
+
if (pointsToConsume <= 0 || !Number.isFinite(pointsToConsume)) {
|
|
98
|
+
throw new BusinessException(`\u6D88\u8D39\u70B9\u6570\u65E0\u6548: ${pointsToConsume}`, "INVALID_POINTS", 400);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const res = await limiter.consume(storageKey, pointsToConsume);
|
|
102
|
+
return {
|
|
103
|
+
remaining: res.remainingPoints,
|
|
104
|
+
msBeforeNext: res.msBeforeNext,
|
|
105
|
+
consumedPoints: res.consumedPoints
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error && typeof error === "object" && "msBeforeNext" in error) {
|
|
109
|
+
const res = error;
|
|
110
|
+
const retryAfter = Math.ceil(res.msBeforeNext / 1e3);
|
|
111
|
+
this.logger.warn(`\u9650\u6D41\u89E6\u53D1: limiter=${limiterName}, retryAfter=${retryAfter}s`);
|
|
112
|
+
throw new TooManyRequestsException(errorMessage, retryAfter);
|
|
113
|
+
}
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Apply a penalty by consuming extra points, extending the block time.
|
|
119
|
+
* Useful for failed login attempts, abuse detection, etc.
|
|
120
|
+
*/
|
|
121
|
+
async penalty(limiterName, key, points = 1) {
|
|
122
|
+
const limiter = this.getLimiter(limiterName);
|
|
123
|
+
await limiter.penalty(this.toStorageKey(key), points);
|
|
124
|
+
this.logger.log(`\u9650\u6D41\u60E9\u7F5A: limiter=${limiterName}, points=${points}`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Reward by reducing consumed points (opposite of penalty).
|
|
128
|
+
*/
|
|
129
|
+
async reward(limiterName, key, points = 1) {
|
|
130
|
+
const limiter = this.getLimiter(limiterName);
|
|
131
|
+
await limiter.reward(this.toStorageKey(key), points);
|
|
132
|
+
}
|
|
133
|
+
async getRemaining(limiterName, key) {
|
|
134
|
+
const limiter = this.getLimiter(limiterName);
|
|
135
|
+
const res = await limiter.get(this.toStorageKey(key));
|
|
136
|
+
if (!res) {
|
|
137
|
+
const config = this.options.limiters[limiterName];
|
|
138
|
+
return {
|
|
139
|
+
remaining: config.points,
|
|
140
|
+
resetAt: new Date(Date.now() + config.duration * 1e3)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
remaining: res.remainingPoints,
|
|
145
|
+
resetAt: new Date(Date.now() + res.msBeforeNext)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async reset(limiterName, key) {
|
|
149
|
+
const limiter = this.getLimiter(limiterName);
|
|
150
|
+
await limiter.delete(this.toStorageKey(key));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Convenience method for public API rate limiting.
|
|
154
|
+
* Consumes 1 point from the named limiter using the given key.
|
|
155
|
+
* Throws TooManyRequestsException (429) when limit is exceeded.
|
|
156
|
+
*
|
|
157
|
+
* @param key - Unique identifier for the consumer (e.g., IP address)
|
|
158
|
+
* @param limiterName - Name of the registered rate limiter
|
|
159
|
+
*/
|
|
160
|
+
async checkPublicApiLimit(key, limiterName) {
|
|
161
|
+
await this.consume(limiterName, key, 1, "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check AI operation rate limit for a user.
|
|
165
|
+
* Falls back gracefully if the limiter doesn't exist (AI ops are best-effort limited).
|
|
166
|
+
*
|
|
167
|
+
* @param userId - User identifier
|
|
168
|
+
* @param operation - Human-readable description of the AI operation
|
|
169
|
+
* @param points - Points to consume (default: 1)
|
|
170
|
+
*/
|
|
171
|
+
async checkAILimit(userId, operation, points = 1) {
|
|
172
|
+
const limiterName = "ai-default";
|
|
173
|
+
if (!this.has(limiterName)) return;
|
|
174
|
+
await this.consume(limiterName, userId, points, `AI \u64CD\u4F5C"${operation}"\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5`);
|
|
175
|
+
}
|
|
176
|
+
/** Check if a limiter exists by name */
|
|
177
|
+
has(limiterName) {
|
|
178
|
+
return this.limiters.has(limiterName);
|
|
179
|
+
}
|
|
180
|
+
/** Get all registered limiter names */
|
|
181
|
+
getLimiterNames() {
|
|
182
|
+
return [
|
|
183
|
+
...this.limiters.keys()
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
isDistributed() {
|
|
187
|
+
return this.useRedis;
|
|
188
|
+
}
|
|
189
|
+
/** Get a limiter by name, throwing if not found */
|
|
190
|
+
getLimiter(limiterName) {
|
|
191
|
+
const limiter = this.limiters.get(limiterName);
|
|
192
|
+
if (!limiter) {
|
|
193
|
+
throw new NotFoundException(`\u9650\u6D41\u5668 '${limiterName}' \u4E0D\u5B58\u5728 (\u53EF\u7528: ${this.getLimiterNames().join(", ")})`);
|
|
194
|
+
}
|
|
195
|
+
return limiter;
|
|
196
|
+
}
|
|
197
|
+
toStorageKey(key) {
|
|
198
|
+
return deriveRateLimitKey(key, this.options.keyHashSecret);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
RateLimiterService = _ts_decorate([
|
|
202
|
+
Injectable(),
|
|
203
|
+
_ts_metadata("design:type", Function),
|
|
204
|
+
_ts_metadata("design:paramtypes", [
|
|
205
|
+
typeof RateLimiterModuleOptions === "undefined" ? Object : RateLimiterModuleOptions
|
|
206
|
+
])
|
|
207
|
+
], RateLimiterService);
|
|
208
|
+
|
|
209
|
+
// src/rate-limiter.module.ts
|
|
210
|
+
import { Module, Global } from "@nestjs/common";
|
|
211
|
+
import { APP_GUARD } from "@nestjs/core";
|
|
212
|
+
|
|
213
|
+
// src/guards/rate-limit.guard.ts
|
|
214
|
+
import { Injectable as Injectable2, Inject, Optional } from "@nestjs/common";
|
|
215
|
+
import { Reflector } from "@nestjs/core";
|
|
216
|
+
|
|
217
|
+
// src/decorators/rate-limit.decorator.ts
|
|
218
|
+
import { SetMetadata } from "@nestjs/common";
|
|
219
|
+
var RATE_LIMIT_KEY = "ZUCKER_RATE_LIMIT";
|
|
220
|
+
function RateLimit(options) {
|
|
221
|
+
const opts = typeof options === "string" ? {
|
|
222
|
+
limiter: options
|
|
223
|
+
} : options;
|
|
224
|
+
return SetMetadata(RATE_LIMIT_KEY, opts);
|
|
225
|
+
}
|
|
226
|
+
__name(RateLimit, "RateLimit");
|
|
227
|
+
|
|
228
|
+
// src/guards/rate-limit.guard.ts
|
|
229
|
+
function _ts_decorate2(decorators, target, key, desc) {
|
|
230
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
231
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
232
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
233
|
+
} else {
|
|
234
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
235
|
+
if (d = decorators[i]) {
|
|
236
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
241
|
+
}
|
|
242
|
+
__name(_ts_decorate2, "_ts_decorate");
|
|
243
|
+
function _ts_metadata2(metadataKey, metadataValue) {
|
|
244
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
|
|
245
|
+
return Reflect.metadata(metadataKey, metadataValue);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
__name(_ts_metadata2, "_ts_metadata");
|
|
249
|
+
function _ts_param(paramIndex, decorator) {
|
|
250
|
+
return function(target, key) {
|
|
251
|
+
decorator(target, key, paramIndex);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
__name(_ts_param, "_ts_param");
|
|
255
|
+
var RateLimitGuard = class {
|
|
256
|
+
static {
|
|
257
|
+
__name(this, "RateLimitGuard");
|
|
258
|
+
}
|
|
259
|
+
reflector;
|
|
260
|
+
rateLimiterService;
|
|
261
|
+
constructor(reflector, rateLimiterService) {
|
|
262
|
+
this.reflector = reflector;
|
|
263
|
+
this.rateLimiterService = rateLimiterService;
|
|
264
|
+
}
|
|
265
|
+
async canActivate(context) {
|
|
266
|
+
const options = this.reflector.getAllAndOverride(RATE_LIMIT_KEY, [
|
|
267
|
+
context.getHandler(),
|
|
268
|
+
context.getClass()
|
|
269
|
+
]);
|
|
270
|
+
if (!options || !this.rateLimiterService) return true;
|
|
271
|
+
const request = context.switchToHttp().getRequest();
|
|
272
|
+
const key = this.extractKey(request, options);
|
|
273
|
+
try {
|
|
274
|
+
await this.rateLimiterService.consume(options.limiter, key);
|
|
275
|
+
return true;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err instanceof TooManyRequestsException) {
|
|
278
|
+
throw new TooManyRequestsException(options.message ?? "Too many requests", err.retryAfter);
|
|
279
|
+
}
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
extractKey(request, options) {
|
|
284
|
+
const networkKey = request.ip ?? request.socket?.remoteAddress ?? "unknown-client";
|
|
285
|
+
switch (options.keyExtractor) {
|
|
286
|
+
case "#userId":
|
|
287
|
+
return request.user?.id ?? networkKey;
|
|
288
|
+
case "#username":
|
|
289
|
+
return request.user?.username ?? networkKey;
|
|
290
|
+
default:
|
|
291
|
+
return networkKey;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
RateLimitGuard = _ts_decorate2([
|
|
296
|
+
Injectable2(),
|
|
297
|
+
_ts_param(1, Inject("RATE_LIMITER_SERVICE")),
|
|
298
|
+
_ts_param(1, Optional()),
|
|
299
|
+
_ts_metadata2("design:type", Function),
|
|
300
|
+
_ts_metadata2("design:paramtypes", [
|
|
301
|
+
typeof Reflector === "undefined" ? Object : Reflector,
|
|
302
|
+
Object
|
|
303
|
+
])
|
|
304
|
+
], RateLimitGuard);
|
|
305
|
+
|
|
306
|
+
// src/rate-limiter.module.ts
|
|
307
|
+
function _ts_decorate3(decorators, target, key, desc) {
|
|
308
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
309
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
310
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
311
|
+
} else {
|
|
312
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
313
|
+
if (d = decorators[i]) {
|
|
314
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
319
|
+
}
|
|
320
|
+
__name(_ts_decorate3, "_ts_decorate");
|
|
321
|
+
var RATE_LIMITER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("RATE_LIMITER_MODULE_OPTIONS");
|
|
322
|
+
var ZuckerRateLimiterModule = class _ZuckerRateLimiterModule {
|
|
323
|
+
static {
|
|
324
|
+
__name(this, "ZuckerRateLimiterModule");
|
|
325
|
+
}
|
|
326
|
+
static forRoot(options) {
|
|
327
|
+
const providers = [
|
|
328
|
+
{
|
|
329
|
+
provide: RateLimiterService,
|
|
330
|
+
useFactory: /* @__PURE__ */ __name(() => new RateLimiterService(options), "useFactory")
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
provide: "RATE_LIMITER_SERVICE",
|
|
334
|
+
useExisting: RateLimiterService
|
|
335
|
+
},
|
|
336
|
+
RateLimitGuard
|
|
337
|
+
];
|
|
338
|
+
if (options.globalGuard !== false) {
|
|
339
|
+
providers.push({
|
|
340
|
+
provide: APP_GUARD,
|
|
341
|
+
useExisting: RateLimitGuard
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
module: _ZuckerRateLimiterModule,
|
|
346
|
+
global: true,
|
|
347
|
+
providers,
|
|
348
|
+
exports: [
|
|
349
|
+
RateLimiterService,
|
|
350
|
+
RateLimitGuard,
|
|
351
|
+
"RATE_LIMITER_SERVICE"
|
|
352
|
+
]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
static forRootAsync(options) {
|
|
356
|
+
const asyncProviders = this.createAsyncProviders(options);
|
|
357
|
+
const providers = [
|
|
358
|
+
...asyncProviders,
|
|
359
|
+
{
|
|
360
|
+
provide: RateLimiterService,
|
|
361
|
+
useFactory: /* @__PURE__ */ __name((opts) => new RateLimiterService(opts), "useFactory"),
|
|
362
|
+
inject: [
|
|
363
|
+
RATE_LIMITER_MODULE_OPTIONS
|
|
364
|
+
]
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
provide: "RATE_LIMITER_SERVICE",
|
|
368
|
+
useExisting: RateLimiterService
|
|
369
|
+
},
|
|
370
|
+
RateLimitGuard
|
|
371
|
+
];
|
|
372
|
+
if (options.globalGuard !== false) {
|
|
373
|
+
providers.push({
|
|
374
|
+
provide: APP_GUARD,
|
|
375
|
+
useExisting: RateLimitGuard
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
module: _ZuckerRateLimiterModule,
|
|
380
|
+
global: true,
|
|
381
|
+
imports: options.imports ?? [],
|
|
382
|
+
providers,
|
|
383
|
+
exports: [
|
|
384
|
+
RateLimiterService,
|
|
385
|
+
RateLimitGuard,
|
|
386
|
+
"RATE_LIMITER_SERVICE"
|
|
387
|
+
]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
static createAsyncProviders(options) {
|
|
391
|
+
if (options.useFactory) {
|
|
392
|
+
return [
|
|
393
|
+
{
|
|
394
|
+
provide: RATE_LIMITER_MODULE_OPTIONS,
|
|
395
|
+
useFactory: options.useFactory,
|
|
396
|
+
inject: options.inject ?? []
|
|
397
|
+
}
|
|
398
|
+
];
|
|
399
|
+
}
|
|
400
|
+
const optionsProvider = {
|
|
401
|
+
provide: RATE_LIMITER_MODULE_OPTIONS,
|
|
402
|
+
useFactory: /* @__PURE__ */ __name(async (factory) => factory.createRateLimiterOptions(), "useFactory"),
|
|
403
|
+
inject: [
|
|
404
|
+
options.useClass ?? options.useExisting
|
|
405
|
+
]
|
|
406
|
+
};
|
|
407
|
+
if (options.useClass) {
|
|
408
|
+
return [
|
|
409
|
+
{
|
|
410
|
+
provide: options.useClass,
|
|
411
|
+
useClass: options.useClass
|
|
412
|
+
},
|
|
413
|
+
optionsProvider
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
return [
|
|
417
|
+
optionsProvider
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
ZuckerRateLimiterModule = _ts_decorate3([
|
|
422
|
+
Global(),
|
|
423
|
+
Module({})
|
|
424
|
+
], ZuckerRateLimiterModule);
|
|
425
|
+
|
|
426
|
+
// src/dynamic-limiter.ts
|
|
427
|
+
import { Injectable as Injectable3, Logger as Logger2 } from "@nestjs/common";
|
|
428
|
+
function _ts_decorate4(decorators, target, key, desc) {
|
|
429
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
430
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
|
|
431
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
432
|
+
} else {
|
|
433
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
434
|
+
if (d = decorators[i]) {
|
|
435
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
440
|
+
}
|
|
441
|
+
__name(_ts_decorate4, "_ts_decorate");
|
|
442
|
+
var DynamicLimiterManager = class _DynamicLimiterManager {
|
|
443
|
+
static {
|
|
444
|
+
__name(this, "DynamicLimiterManager");
|
|
445
|
+
}
|
|
446
|
+
logger = new Logger2(_DynamicLimiterManager.name);
|
|
447
|
+
rules = /* @__PURE__ */ new Map();
|
|
448
|
+
listeners = [];
|
|
449
|
+
/** 添加或更新限流规则 */
|
|
450
|
+
setRule(rule) {
|
|
451
|
+
rule.createdAt = rule.createdAt ?? Date.now();
|
|
452
|
+
this.rules.set(rule.id, rule);
|
|
453
|
+
this.notifyListeners();
|
|
454
|
+
this.logger.log(`Dynamic limiter rule set: ${rule.id} (${rule.points}/${rule.duration}s)`);
|
|
455
|
+
}
|
|
456
|
+
/** 删除限流规则 */
|
|
457
|
+
removeRule(id) {
|
|
458
|
+
const removed = this.rules.delete(id);
|
|
459
|
+
if (removed) {
|
|
460
|
+
this.notifyListeners();
|
|
461
|
+
this.logger.log(`Dynamic limiter rule removed: ${id}`);
|
|
462
|
+
}
|
|
463
|
+
return removed;
|
|
464
|
+
}
|
|
465
|
+
/** 获取所有规则(按优先级排序) */
|
|
466
|
+
getRules() {
|
|
467
|
+
return Array.from(this.rules.values()).filter((r) => r.enabled).sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
468
|
+
}
|
|
469
|
+
/** 根据路径和角色匹配限流规则 */
|
|
470
|
+
matchRule(path, roles) {
|
|
471
|
+
for (const rule of this.getRules()) {
|
|
472
|
+
if (rule.pathPattern) {
|
|
473
|
+
const regex = this.globToRegex(rule.pathPattern);
|
|
474
|
+
if (!regex.test(path)) continue;
|
|
475
|
+
}
|
|
476
|
+
if (rule.roles?.length) {
|
|
477
|
+
if (!roles?.some((r) => rule.roles.includes(r))) continue;
|
|
478
|
+
}
|
|
479
|
+
return rule;
|
|
480
|
+
}
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
/** 批量导入规则 */
|
|
484
|
+
importRules(rules) {
|
|
485
|
+
for (const rule of rules) {
|
|
486
|
+
this.rules.set(rule.id, {
|
|
487
|
+
...rule,
|
|
488
|
+
createdAt: rule.createdAt ?? Date.now()
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
this.notifyListeners();
|
|
492
|
+
this.logger.log(`Imported ${rules.length} dynamic limiter rules`);
|
|
493
|
+
}
|
|
494
|
+
/** 导出所有规则 */
|
|
495
|
+
exportRules() {
|
|
496
|
+
return Array.from(this.rules.values());
|
|
497
|
+
}
|
|
498
|
+
/** 监听规则变更 */
|
|
499
|
+
onRulesChanged(listener) {
|
|
500
|
+
this.listeners.push(listener);
|
|
501
|
+
return {
|
|
502
|
+
unsubscribe: /* @__PURE__ */ __name(() => {
|
|
503
|
+
const idx = this.listeners.indexOf(listener);
|
|
504
|
+
if (idx >= 0) this.listeners.splice(idx, 1);
|
|
505
|
+
}, "unsubscribe")
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/** 启用/禁用规则 */
|
|
509
|
+
toggleRule(id, enabled) {
|
|
510
|
+
const rule = this.rules.get(id);
|
|
511
|
+
if (!rule) return false;
|
|
512
|
+
rule.enabled = enabled;
|
|
513
|
+
this.notifyListeners();
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
notifyListeners() {
|
|
517
|
+
const rules = this.getRules();
|
|
518
|
+
for (const listener of this.listeners) {
|
|
519
|
+
try {
|
|
520
|
+
listener(rules);
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
globToRegex(glob) {
|
|
526
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
527
|
+
return new RegExp(`^${escaped}$`);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
DynamicLimiterManager = _ts_decorate4([
|
|
531
|
+
Injectable3()
|
|
532
|
+
], DynamicLimiterManager);
|
|
533
|
+
export {
|
|
534
|
+
DynamicLimiterManager,
|
|
535
|
+
RATE_LIMIT_KEY,
|
|
536
|
+
RateLimit,
|
|
537
|
+
RateLimitGuard,
|
|
538
|
+
RateLimiterService,
|
|
539
|
+
TooManyRequestsException,
|
|
540
|
+
ZuckerRateLimiterModule,
|
|
541
|
+
deriveRateLimitKey
|
|
542
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zucker-framework/rate-limiter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"module": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"NOTICE",
|
|
11
|
+
"THIRD_PARTY_NOTICES"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@zucker-framework/core": "1.0.0",
|
|
15
|
+
"rate-limiter-flexible": "^11.2.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@nestjs/common": "^12.0.0",
|
|
19
|
+
"@nestjs/core": "^12.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.19.43"
|
|
23
|
+
},
|
|
24
|
+
"license": "Apache-2.0",
|
|
25
|
+
"author": "Lind",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Lind-pro/zucker-framework.git",
|
|
29
|
+
"directory": "packages/rate-limiter"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --external @nestjs/common --external @nestjs/core --external @zucker-framework/core --external rate-limiter-flexible",
|
|
37
|
+
"clean": "rm -rf dist",
|
|
38
|
+
"lint": "eslint src/"
|
|
39
|
+
}
|
|
40
|
+
}
|