@grupodiariodaregiao/bunstone 0.3.3 → 0.3.4
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/dist/index.d.ts +1 -1
- package/dist/index.js +353 -26
- package/dist/lib/app-startup.d.ts +6 -0
- package/dist/lib/ratelimit/constants.d.ts +12 -0
- package/dist/lib/ratelimit/index.d.ts +6 -0
- package/dist/lib/ratelimit/interfaces/storage.interface.d.ts +41 -0
- package/dist/lib/ratelimit/ratelimit.decorator.d.ts +52 -0
- package/dist/lib/ratelimit/ratelimit.service.d.ts +88 -0
- package/dist/lib/ratelimit/storage/memory.storage.d.ts +28 -0
- package/dist/lib/ratelimit/storage/redis.storage.d.ts +47 -0
- package/dist/lib/types/options.d.ts +27 -0
- package/lib/app-startup.ts +113 -5
- package/lib/bullmq/queue.service.ts +3 -2
- package/lib/cqrs/decorators/command-handler.decorator.ts +3 -3
- package/lib/cqrs/decorators/event-handler.decorator.ts +3 -3
- package/lib/cqrs/decorators/query-handler.decorator.ts +3 -3
- package/lib/cqrs/decorators/saga.decorator.ts +19 -19
- package/lib/cqrs/event-bus.ts +78 -78
- package/lib/injectable.ts +3 -3
- package/lib/module.ts +51 -30
- package/lib/openapi.ts +117 -117
- package/lib/ratelimit/constants.ts +15 -0
- package/lib/ratelimit/index.ts +6 -0
- package/lib/ratelimit/interfaces/storage.interface.ts +49 -0
- package/lib/ratelimit/ratelimit.decorator.ts +86 -0
- package/lib/ratelimit/ratelimit.service.ts +228 -0
- package/lib/ratelimit/storage/memory.storage.ts +98 -0
- package/lib/ratelimit/storage/redis.storage.ts +134 -0
- package/lib/schedule/cron/cron.ts +26 -26
- package/lib/schedule/cron/mappers/map-providers-with-cron.ts +3 -3
- package/lib/schedule/timeout/mappers/map-providers-with-timeouts.ts +3 -3
- package/lib/schedule/timeout/timeout.ts +21 -21
- package/lib/types/options.ts +28 -0
- package/package.json +2 -1
- package/starter/biome.json +0 -42
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./lib/adapters/upload-adapter";
|
|
|
7
7
|
export * from "./lib/app-startup";
|
|
8
8
|
export * from "./lib/components/layout";
|
|
9
9
|
export * from "./lib/controller";
|
|
10
|
+
export * from "./lib/ratelimit";
|
|
10
11
|
export * from "./lib/cqrs/command-bus";
|
|
11
12
|
export * from "./lib/cqrs/cqrs-module";
|
|
12
13
|
export * from "./lib/cqrs/decorators/command-handler.decorator";
|
|
@@ -14,7 +15,6 @@ export * from "./lib/cqrs/decorators/event-handler.decorator";
|
|
|
14
15
|
export * from "./lib/cqrs/decorators/query-handler.decorator";
|
|
15
16
|
export * from "./lib/cqrs/decorators/saga.decorator";
|
|
16
17
|
export * from "./lib/cqrs/event-bus";
|
|
17
|
-
export { map, ofType } from "./lib/cqrs/event-bus";
|
|
18
18
|
export * from "./lib/cqrs/interfaces/command.interface";
|
|
19
19
|
export * from "./lib/cqrs/interfaces/event.interface";
|
|
20
20
|
export * from "./lib/cqrs/interfaces/query.interface";
|
package/dist/index.js
CHANGED
|
@@ -32209,7 +32209,7 @@ var require_node_cron = __commonJS((exports) => {
|
|
|
32209
32209
|
});
|
|
32210
32210
|
|
|
32211
32211
|
// index.ts
|
|
32212
|
-
var
|
|
32212
|
+
var import_reflect_metadata25 = __toESM(require_Reflect(), 1);
|
|
32213
32213
|
|
|
32214
32214
|
// lib/adapters/cache-adapter.ts
|
|
32215
32215
|
var {RedisClient, redis } = globalThis.Bun;
|
|
@@ -42871,6 +42871,14 @@ var MapProvidersWithBullMq = {
|
|
|
42871
42871
|
}
|
|
42872
42872
|
};
|
|
42873
42873
|
|
|
42874
|
+
// lib/ratelimit/constants.ts
|
|
42875
|
+
var RATELIMIT_METADATA_KEY = "dip:ratelimit";
|
|
42876
|
+
var RATELIMIT_CONTROLLER_METADATA_KEY = "dip:ratelimit:controller";
|
|
42877
|
+
var DEFAULT_RATELIMIT_CONFIG = {
|
|
42878
|
+
max: 100,
|
|
42879
|
+
windowMs: 60000
|
|
42880
|
+
};
|
|
42881
|
+
|
|
42874
42882
|
// lib/schedule/cron/mappers/map-providers-with-cron.ts
|
|
42875
42883
|
var MapProvidersWithCron = {
|
|
42876
42884
|
execute(providers = []) {
|
|
@@ -43082,21 +43090,24 @@ function mapControllers(controllers = []) {
|
|
|
43082
43090
|
const controllersMap = new Map;
|
|
43083
43091
|
for (const controller of controllers) {
|
|
43084
43092
|
controllersMap.set(controller, []);
|
|
43085
|
-
|
|
43086
|
-
|
|
43087
|
-
|
|
43088
|
-
|
|
43089
|
-
|
|
43090
|
-
|
|
43091
|
-
|
|
43092
|
-
|
|
43093
|
-
|
|
43094
|
-
|
|
43095
|
-
|
|
43096
|
-
|
|
43097
|
-
|
|
43093
|
+
const controllerRateLimit = Reflect.getMetadata(RATELIMIT_CONTROLLER_METADATA_KEY, controller);
|
|
43094
|
+
const controllerPathname = Reflect.getOwnMetadata("dip:controller:pathname", controller);
|
|
43095
|
+
const controllerGuard = Reflect.getMetadata("dip:guard", controller);
|
|
43096
|
+
const methodsSymbol = Symbol.for("dip:controller:http-methods");
|
|
43097
|
+
const controllerMethods = controller[methodsSymbol] || [];
|
|
43098
|
+
controllerMethods.forEach((cm) => {
|
|
43099
|
+
const pathname = `${controllerPathname === "/" ? "" : controllerPathname}${cm.pathname}`;
|
|
43100
|
+
const methodGuard = Reflect.getMetadata("dip:guard", controller.prototype, cm.methodName);
|
|
43101
|
+
const methodRateLimit = Reflect.getMetadata(RATELIMIT_METADATA_KEY, controller.prototype[cm.methodName]);
|
|
43102
|
+
const effectiveRateLimit = methodRateLimit || controllerRateLimit;
|
|
43103
|
+
controllersMap.get(controller)?.push({
|
|
43104
|
+
httpMethod: cm.httpMethod,
|
|
43105
|
+
pathname,
|
|
43106
|
+
methodName: cm.methodName,
|
|
43107
|
+
guard: methodGuard || controllerGuard,
|
|
43108
|
+
rateLimit: effectiveRateLimit
|
|
43098
43109
|
});
|
|
43099
|
-
}
|
|
43110
|
+
});
|
|
43100
43111
|
}
|
|
43101
43112
|
return controllersMap;
|
|
43102
43113
|
}
|
|
@@ -102964,8 +102975,9 @@ class QueueService {
|
|
|
102964
102975
|
return await queue2.add(jobName, data, opts);
|
|
102965
102976
|
}
|
|
102966
102977
|
getQueue(queueName) {
|
|
102967
|
-
|
|
102968
|
-
|
|
102978
|
+
const existingQueue = this.queues.get(queueName);
|
|
102979
|
+
if (existingQueue) {
|
|
102980
|
+
return existingQueue;
|
|
102969
102981
|
}
|
|
102970
102982
|
if (!QueueService.redisOptions) {
|
|
102971
102983
|
this.logger.error(`Redis options not set for BullMQ. Ensure BullMqModule.register() is called in your AppModule imports.`);
|
|
@@ -103335,6 +103347,160 @@ function ApiHeaders(headers) {
|
|
|
103335
103347
|
};
|
|
103336
103348
|
}
|
|
103337
103349
|
|
|
103350
|
+
// lib/ratelimit/storage/memory.storage.ts
|
|
103351
|
+
class MemoryStorage {
|
|
103352
|
+
storage = new Map;
|
|
103353
|
+
cleanupInterval = null;
|
|
103354
|
+
constructor() {
|
|
103355
|
+
this.cleanupInterval = setInterval(() => {
|
|
103356
|
+
const now = Date.now();
|
|
103357
|
+
for (const [key, entry] of this.storage.entries()) {
|
|
103358
|
+
if (entry.resetTime <= now) {
|
|
103359
|
+
this.storage.delete(key);
|
|
103360
|
+
}
|
|
103361
|
+
}
|
|
103362
|
+
}, 5 * 60 * 1000);
|
|
103363
|
+
}
|
|
103364
|
+
increment(key, windowMs) {
|
|
103365
|
+
const now = Date.now();
|
|
103366
|
+
const entry = this.storage.get(key);
|
|
103367
|
+
if (!entry || entry.resetTime <= now) {
|
|
103368
|
+
const newEntry = {
|
|
103369
|
+
count: 1,
|
|
103370
|
+
resetTime: now + windowMs
|
|
103371
|
+
};
|
|
103372
|
+
this.storage.set(key, newEntry);
|
|
103373
|
+
return {
|
|
103374
|
+
totalHits: 1,
|
|
103375
|
+
remaining: Infinity,
|
|
103376
|
+
resetTime: newEntry.resetTime
|
|
103377
|
+
};
|
|
103378
|
+
}
|
|
103379
|
+
entry.count++;
|
|
103380
|
+
return {
|
|
103381
|
+
totalHits: entry.count,
|
|
103382
|
+
remaining: Infinity,
|
|
103383
|
+
resetTime: entry.resetTime
|
|
103384
|
+
};
|
|
103385
|
+
}
|
|
103386
|
+
decrement(key) {
|
|
103387
|
+
const entry = this.storage.get(key);
|
|
103388
|
+
if (entry && entry.count > 0) {
|
|
103389
|
+
entry.count--;
|
|
103390
|
+
}
|
|
103391
|
+
}
|
|
103392
|
+
resetKey(key) {
|
|
103393
|
+
this.storage.delete(key);
|
|
103394
|
+
}
|
|
103395
|
+
close() {
|
|
103396
|
+
if (this.cleanupInterval) {
|
|
103397
|
+
clearInterval(this.cleanupInterval);
|
|
103398
|
+
this.cleanupInterval = null;
|
|
103399
|
+
}
|
|
103400
|
+
this.storage.clear();
|
|
103401
|
+
}
|
|
103402
|
+
}
|
|
103403
|
+
|
|
103404
|
+
// lib/ratelimit/ratelimit.service.ts
|
|
103405
|
+
class RateLimitExceededException extends Error {
|
|
103406
|
+
limit;
|
|
103407
|
+
resetTime;
|
|
103408
|
+
constructor(limit, resetTime) {
|
|
103409
|
+
super(`Rate limit exceeded. Limit: ${limit}. Retry after ${new Date(resetTime).toISOString()}`);
|
|
103410
|
+
this.limit = limit;
|
|
103411
|
+
this.resetTime = resetTime;
|
|
103412
|
+
this.name = "RateLimitExceededException";
|
|
103413
|
+
}
|
|
103414
|
+
}
|
|
103415
|
+
|
|
103416
|
+
class RateLimitService {
|
|
103417
|
+
defaultStorage = new MemoryStorage;
|
|
103418
|
+
async process(req, config) {
|
|
103419
|
+
if (config.skip?.(req)) {
|
|
103420
|
+
return this.createSkipResult(config.max);
|
|
103421
|
+
}
|
|
103422
|
+
if (config.skipHeader && req.headers[config.skipHeader.toLowerCase()]) {
|
|
103423
|
+
return this.createSkipResult(config.max);
|
|
103424
|
+
}
|
|
103425
|
+
const storage = config.storage || this.defaultStorage;
|
|
103426
|
+
const key = this.generateKey(req, config.keyGenerator);
|
|
103427
|
+
const info = await storage.increment(key, config.windowMs);
|
|
103428
|
+
info.remaining = Math.max(0, config.max - info.totalHits);
|
|
103429
|
+
const allowed = info.totalHits <= config.max;
|
|
103430
|
+
const headers = this.generateHeaders(info, config.max, allowed);
|
|
103431
|
+
return {
|
|
103432
|
+
allowed,
|
|
103433
|
+
headers,
|
|
103434
|
+
info
|
|
103435
|
+
};
|
|
103436
|
+
}
|
|
103437
|
+
async decrement(req, config) {
|
|
103438
|
+
const storage = config.storage || this.defaultStorage;
|
|
103439
|
+
const key = this.generateKey(req, config.keyGenerator);
|
|
103440
|
+
await storage.decrement(key);
|
|
103441
|
+
}
|
|
103442
|
+
generateKey(req, keyGenerator) {
|
|
103443
|
+
if (keyGenerator) {
|
|
103444
|
+
return keyGenerator(req);
|
|
103445
|
+
}
|
|
103446
|
+
const ip = this.extractIp(req);
|
|
103447
|
+
const method = req.request?.method || req.method || "GET";
|
|
103448
|
+
const path3 = req.request?.url || req.path || req.url || "/";
|
|
103449
|
+
return `${ip}:${method}:${path3}`;
|
|
103450
|
+
}
|
|
103451
|
+
extractIp(req) {
|
|
103452
|
+
const forwarded = req.headers["x-forwarded-for"];
|
|
103453
|
+
if (forwarded && typeof forwarded === "string") {
|
|
103454
|
+
const firstIp = forwarded.split(",")[0];
|
|
103455
|
+
if (firstIp) {
|
|
103456
|
+
return firstIp.trim();
|
|
103457
|
+
}
|
|
103458
|
+
}
|
|
103459
|
+
const realIp = req.headers["x-real-ip"];
|
|
103460
|
+
if (realIp && typeof realIp === "string") {
|
|
103461
|
+
return realIp;
|
|
103462
|
+
}
|
|
103463
|
+
const ip = req.ip || req.request?.ip;
|
|
103464
|
+
if (ip) {
|
|
103465
|
+
return ip;
|
|
103466
|
+
}
|
|
103467
|
+
return "unknown";
|
|
103468
|
+
}
|
|
103469
|
+
generateHeaders(info, max, allowed) {
|
|
103470
|
+
const headers = {
|
|
103471
|
+
"X-RateLimit-Limit": max.toString(),
|
|
103472
|
+
"X-RateLimit-Remaining": info.remaining.toString(),
|
|
103473
|
+
"X-RateLimit-Reset": Math.ceil(info.resetTime / 1000).toString()
|
|
103474
|
+
};
|
|
103475
|
+
if (!allowed) {
|
|
103476
|
+
const retryAfterSeconds = Math.ceil((info.resetTime - Date.now()) / 1000);
|
|
103477
|
+
headers["Retry-After"] = Math.max(0, retryAfterSeconds).toString();
|
|
103478
|
+
}
|
|
103479
|
+
return headers;
|
|
103480
|
+
}
|
|
103481
|
+
createSkipResult(max) {
|
|
103482
|
+
return {
|
|
103483
|
+
allowed: true,
|
|
103484
|
+
headers: {
|
|
103485
|
+
"X-RateLimit-Limit": max.toString(),
|
|
103486
|
+
"X-RateLimit-Remaining": max.toString(),
|
|
103487
|
+
"X-RateLimit-Reset": "0"
|
|
103488
|
+
},
|
|
103489
|
+
info: {
|
|
103490
|
+
totalHits: 0,
|
|
103491
|
+
remaining: max,
|
|
103492
|
+
resetTime: Date.now()
|
|
103493
|
+
}
|
|
103494
|
+
};
|
|
103495
|
+
}
|
|
103496
|
+
getDefaultStorage() {
|
|
103497
|
+
return this.defaultStorage;
|
|
103498
|
+
}
|
|
103499
|
+
}
|
|
103500
|
+
RateLimitService = __legacyDecorateClassTS([
|
|
103501
|
+
Injectable()
|
|
103502
|
+
], RateLimitService);
|
|
103503
|
+
|
|
103338
103504
|
// lib/render.ts
|
|
103339
103505
|
var import_reflect_metadata14 = __toESM(require_Reflect(), 1);
|
|
103340
103506
|
var RENDER_METADATA = "dip:render:component";
|
|
@@ -103356,6 +103522,8 @@ class AppStartup {
|
|
|
103356
103522
|
static logger = new Logger(AppStartup.name);
|
|
103357
103523
|
static registeredSagas = new WeakSet;
|
|
103358
103524
|
static viewBundles = new Map;
|
|
103525
|
+
static globalRateLimitConfig;
|
|
103526
|
+
static rateLimitService = new RateLimitService;
|
|
103359
103527
|
static async create(module, options) {
|
|
103360
103528
|
try {
|
|
103361
103529
|
AppStartup.elysia = new Elysia;
|
|
@@ -103417,6 +103585,7 @@ class AppStartup {
|
|
|
103417
103585
|
documentation: options.swagger.documentation
|
|
103418
103586
|
}));
|
|
103419
103587
|
}
|
|
103588
|
+
AppStartup.globalRateLimitConfig = options?.rateLimit;
|
|
103420
103589
|
AppStartup.registerModules(module);
|
|
103421
103590
|
return {
|
|
103422
103591
|
listen: AppStartup.listen,
|
|
@@ -103627,6 +103796,23 @@ if (document.readyState === 'loading') {
|
|
|
103627
103796
|
} : undefined
|
|
103628
103797
|
};
|
|
103629
103798
|
});
|
|
103799
|
+
const hasRateLimit = method.rateLimit || AppStartup.globalRateLimitConfig?.enabled !== false && AppStartup.globalRateLimitConfig;
|
|
103800
|
+
if (hasRateLimit) {
|
|
103801
|
+
responses["429"] = {
|
|
103802
|
+
description: "Too Many Requests - Rate limit exceeded",
|
|
103803
|
+
content: {
|
|
103804
|
+
"application/json": {
|
|
103805
|
+
schema: {
|
|
103806
|
+
type: "object",
|
|
103807
|
+
properties: {
|
|
103808
|
+
status: { type: "number" },
|
|
103809
|
+
message: { type: "string" }
|
|
103810
|
+
}
|
|
103811
|
+
}
|
|
103812
|
+
}
|
|
103813
|
+
}
|
|
103814
|
+
};
|
|
103815
|
+
}
|
|
103630
103816
|
const controllerHeaders = Reflect.getMetadata(API_HEADERS_METADATA, controllerInstance) || [];
|
|
103631
103817
|
const methodHeaders = Reflect.getMetadata(API_HEADERS_METADATA, controllerInstance.prototype, method.methodName) || [];
|
|
103632
103818
|
const allHeaders = [...controllerHeaders, ...methodHeaders];
|
|
@@ -103647,6 +103833,7 @@ if (document.readyState === 'loading') {
|
|
|
103647
103833
|
const guardDependencies = resolveDependencies(guardParamsTypes, injectables);
|
|
103648
103834
|
guardInstance = new method.guard(...guardDependencies);
|
|
103649
103835
|
}
|
|
103836
|
+
const effectiveRateLimit = AppStartup.buildEffectiveRateLimit(method.rateLimit);
|
|
103650
103837
|
AppStartup.elysia[httpMethod](method.pathname, (req) => AppStartup.executeControllerMethod(req, controller, method.methodName), {
|
|
103651
103838
|
body: bodySchema,
|
|
103652
103839
|
query: querySchema,
|
|
@@ -103658,7 +103845,24 @@ if (document.readyState === 'loading') {
|
|
|
103658
103845
|
responses,
|
|
103659
103846
|
parameters
|
|
103660
103847
|
},
|
|
103661
|
-
beforeHandle(req) {
|
|
103848
|
+
async beforeHandle(req) {
|
|
103849
|
+
if (effectiveRateLimit) {
|
|
103850
|
+
const result = await AppStartup.rateLimitService.process(req, effectiveRateLimit);
|
|
103851
|
+
if (req.set?.headers) {
|
|
103852
|
+
Object.entries(result.headers).forEach(([key, value]) => {
|
|
103853
|
+
if (value !== undefined) {
|
|
103854
|
+
req.set.headers[key] = value;
|
|
103855
|
+
}
|
|
103856
|
+
});
|
|
103857
|
+
}
|
|
103858
|
+
if (!result.allowed) {
|
|
103859
|
+
req.set.status = 429;
|
|
103860
|
+
return {
|
|
103861
|
+
status: 429,
|
|
103862
|
+
message: effectiveRateLimit.message || "Too many requests, please try again later."
|
|
103863
|
+
};
|
|
103864
|
+
}
|
|
103865
|
+
}
|
|
103662
103866
|
if (!guardInstance)
|
|
103663
103867
|
return;
|
|
103664
103868
|
const isValid = guardInstance.validate(req);
|
|
@@ -103678,6 +103882,37 @@ if (document.readyState === 'loading') {
|
|
|
103678
103882
|
}
|
|
103679
103883
|
}
|
|
103680
103884
|
}
|
|
103885
|
+
static buildEffectiveRateLimit(methodRateLimit) {
|
|
103886
|
+
const global2 = AppStartup.globalRateLimitConfig;
|
|
103887
|
+
if (methodRateLimit) {
|
|
103888
|
+
if (methodRateLimit.enabled === false) {
|
|
103889
|
+
return;
|
|
103890
|
+
}
|
|
103891
|
+
return {
|
|
103892
|
+
enabled: true,
|
|
103893
|
+
max: methodRateLimit.max ?? global2?.max ?? 100,
|
|
103894
|
+
windowMs: methodRateLimit.windowMs ?? global2?.windowMs ?? 60000,
|
|
103895
|
+
storage: methodRateLimit.storage ?? global2?.storage,
|
|
103896
|
+
keyGenerator: methodRateLimit.keyGenerator ?? global2?.keyGenerator,
|
|
103897
|
+
skipHeader: methodRateLimit.skipHeader ?? global2?.skipHeader,
|
|
103898
|
+
skip: methodRateLimit.skip ?? global2?.skip,
|
|
103899
|
+
message: methodRateLimit.message ?? global2?.message
|
|
103900
|
+
};
|
|
103901
|
+
}
|
|
103902
|
+
if (global2?.enabled !== false && global2) {
|
|
103903
|
+
return {
|
|
103904
|
+
enabled: true,
|
|
103905
|
+
max: global2.max ?? 100,
|
|
103906
|
+
windowMs: global2.windowMs ?? 60000,
|
|
103907
|
+
storage: global2.storage,
|
|
103908
|
+
keyGenerator: global2.keyGenerator,
|
|
103909
|
+
skipHeader: global2.skipHeader,
|
|
103910
|
+
skip: global2.skip,
|
|
103911
|
+
message: global2.message
|
|
103912
|
+
};
|
|
103913
|
+
}
|
|
103914
|
+
return;
|
|
103915
|
+
}
|
|
103681
103916
|
static registerTimeouts(module) {
|
|
103682
103917
|
const providersTimeouts = Reflect.getMetadata("dip:timeouts", module);
|
|
103683
103918
|
if (!providersTimeouts) {
|
|
@@ -103718,7 +103953,7 @@ if (document.readyState === 'loading') {
|
|
|
103718
103953
|
return;
|
|
103719
103954
|
}
|
|
103720
103955
|
const injectables = Reflect.getMetadata("dip:injectables", module);
|
|
103721
|
-
const
|
|
103956
|
+
const _queueService = injectables?.get(QueueService);
|
|
103722
103957
|
const redisOptions = QueueService.redisOptions;
|
|
103723
103958
|
for (const item of providersBullMq.entries()) {
|
|
103724
103959
|
const [providerClass, methods] = item;
|
|
@@ -103849,6 +104084,90 @@ function Controller(pathname = "/") {
|
|
|
103849
104084
|
Reflect.defineMetadata("dip:controller:pathname", pathname, target2);
|
|
103850
104085
|
};
|
|
103851
104086
|
}
|
|
104087
|
+
// lib/ratelimit/ratelimit.decorator.ts
|
|
104088
|
+
var import_reflect_metadata17 = __toESM(require_Reflect(), 1);
|
|
104089
|
+
function RateLimit(options = {}) {
|
|
104090
|
+
return (target2, _propertyKey, descriptor) => {
|
|
104091
|
+
const config = {
|
|
104092
|
+
enabled: options.enabled ?? true,
|
|
104093
|
+
max: options.max ?? 100,
|
|
104094
|
+
windowMs: options.windowMs ?? 60000,
|
|
104095
|
+
message: options.message,
|
|
104096
|
+
storage: options.storage,
|
|
104097
|
+
keyGenerator: options.keyGenerator,
|
|
104098
|
+
skipHeader: options.skipHeader,
|
|
104099
|
+
skip: options.skip
|
|
104100
|
+
};
|
|
104101
|
+
if (descriptor) {
|
|
104102
|
+
Reflect.defineMetadata(RATELIMIT_METADATA_KEY, config, descriptor.value);
|
|
104103
|
+
return descriptor;
|
|
104104
|
+
}
|
|
104105
|
+
Reflect.defineMetadata(RATELIMIT_CONTROLLER_METADATA_KEY, config, target2);
|
|
104106
|
+
return target2;
|
|
104107
|
+
};
|
|
104108
|
+
}
|
|
104109
|
+
// lib/ratelimit/storage/redis.storage.ts
|
|
104110
|
+
class RedisStorage {
|
|
104111
|
+
redisClient;
|
|
104112
|
+
prefix = "ratelimit:";
|
|
104113
|
+
constructor(redisClient, prefix) {
|
|
104114
|
+
this.redisClient = redisClient;
|
|
104115
|
+
if (prefix) {
|
|
104116
|
+
this.prefix = prefix;
|
|
104117
|
+
}
|
|
104118
|
+
}
|
|
104119
|
+
getKey(key) {
|
|
104120
|
+
return `${this.prefix}${key}`;
|
|
104121
|
+
}
|
|
104122
|
+
async increment(key, windowMs) {
|
|
104123
|
+
const fullKey = this.getKey(key);
|
|
104124
|
+
const now = Date.now();
|
|
104125
|
+
const multi = this.redisClient.multi();
|
|
104126
|
+
multi.incr(fullKey);
|
|
104127
|
+
multi.pexpire(fullKey, windowMs);
|
|
104128
|
+
multi.pttl(fullKey);
|
|
104129
|
+
const result = await multi.exec();
|
|
104130
|
+
if (!result) {
|
|
104131
|
+
throw new Error("Failed to execute Redis transaction");
|
|
104132
|
+
}
|
|
104133
|
+
const results = result[1];
|
|
104134
|
+
if (!results || !Array.isArray(results)) {
|
|
104135
|
+
throw new Error("Invalid Redis transaction response");
|
|
104136
|
+
}
|
|
104137
|
+
const countResult = results[0];
|
|
104138
|
+
const ttlResult = results[2];
|
|
104139
|
+
const incrError = countResult[0];
|
|
104140
|
+
const count = countResult[1];
|
|
104141
|
+
if (incrError) {
|
|
104142
|
+
throw incrError;
|
|
104143
|
+
}
|
|
104144
|
+
const ttl = ttlResult?.[1] ?? -1;
|
|
104145
|
+
let resetTime;
|
|
104146
|
+
if (typeof ttl === "number" && ttl > 0) {
|
|
104147
|
+
resetTime = now + ttl;
|
|
104148
|
+
} else {
|
|
104149
|
+
resetTime = now + windowMs;
|
|
104150
|
+
}
|
|
104151
|
+
return {
|
|
104152
|
+
totalHits: typeof count === "string" ? Number.parseInt(count, 10) : count || 1,
|
|
104153
|
+
remaining: Infinity,
|
|
104154
|
+
resetTime
|
|
104155
|
+
};
|
|
104156
|
+
}
|
|
104157
|
+
async decrement(key) {
|
|
104158
|
+
const fullKey = this.getKey(key);
|
|
104159
|
+
await this.redisClient.decr(fullKey);
|
|
104160
|
+
}
|
|
104161
|
+
async resetKey(key) {
|
|
104162
|
+
const fullKey = this.getKey(key);
|
|
104163
|
+
await this.redisClient.del(fullKey);
|
|
104164
|
+
}
|
|
104165
|
+
async close() {
|
|
104166
|
+
if (this.redisClient.quit) {
|
|
104167
|
+
await this.redisClient.quit();
|
|
104168
|
+
}
|
|
104169
|
+
}
|
|
104170
|
+
}
|
|
103852
104171
|
// lib/cqrs/cqrs-module.ts
|
|
103853
104172
|
class CqrsModule {
|
|
103854
104173
|
}
|
|
@@ -103923,7 +104242,7 @@ BullMqModule = __legacyDecorateClassTS([
|
|
|
103923
104242
|
})
|
|
103924
104243
|
], BullMqModule);
|
|
103925
104244
|
// lib/bullmq/decorators/processor.decorator.ts
|
|
103926
|
-
var
|
|
104245
|
+
var import_reflect_metadata18 = __toESM(require_Reflect(), 1);
|
|
103927
104246
|
function Processor(options) {
|
|
103928
104247
|
const processorOptions = typeof options === "string" ? { queueName: options } : options;
|
|
103929
104248
|
return (target2) => {
|
|
@@ -103932,7 +104251,7 @@ function Processor(options) {
|
|
|
103932
104251
|
};
|
|
103933
104252
|
}
|
|
103934
104253
|
// lib/bullmq/decorators/process.decorator.ts
|
|
103935
|
-
var
|
|
104254
|
+
var import_reflect_metadata19 = __toESM(require_Reflect(), 1);
|
|
103936
104255
|
function Process(name3) {
|
|
103937
104256
|
return (target2, propertyKey, _descriptor) => {
|
|
103938
104257
|
const sym = Symbol.for("dip:providers:bullmq");
|
|
@@ -103946,7 +104265,7 @@ function Process(name3) {
|
|
|
103946
104265
|
};
|
|
103947
104266
|
}
|
|
103948
104267
|
// lib/guard.ts
|
|
103949
|
-
var
|
|
104268
|
+
var import_reflect_metadata20 = __toESM(require_Reflect(), 1);
|
|
103950
104269
|
|
|
103951
104270
|
// lib/utils/is-class.ts
|
|
103952
104271
|
function isClass(fn3) {
|
|
@@ -104018,7 +104337,7 @@ function Jwt() {
|
|
|
104018
104337
|
};
|
|
104019
104338
|
}
|
|
104020
104339
|
// lib/jwt/jwt-module.ts
|
|
104021
|
-
var
|
|
104340
|
+
var import_reflect_metadata21 = __toESM(require_Reflect(), 1);
|
|
104022
104341
|
|
|
104023
104342
|
class JwtModule {
|
|
104024
104343
|
static options;
|
|
@@ -104034,7 +104353,7 @@ class JwtModule {
|
|
|
104034
104353
|
}
|
|
104035
104354
|
}
|
|
104036
104355
|
// lib/schedule/cron/cron.ts
|
|
104037
|
-
var
|
|
104356
|
+
var import_reflect_metadata22 = __toESM(require_Reflect(), 1);
|
|
104038
104357
|
function Cron(expression) {
|
|
104039
104358
|
if (!expression) {
|
|
104040
104359
|
throw new Error("Invalid cron expression.");
|
|
@@ -104054,7 +104373,7 @@ function Cron(expression) {
|
|
|
104054
104373
|
};
|
|
104055
104374
|
}
|
|
104056
104375
|
// lib/schedule/timeout/timeout.ts
|
|
104057
|
-
var
|
|
104376
|
+
var import_reflect_metadata23 = __toESM(require_Reflect(), 1);
|
|
104058
104377
|
function Timeout(delay2) {
|
|
104059
104378
|
if (!delay2 || delay2 < 0) {
|
|
104060
104379
|
throw new Error("Delay must be a positive number.");
|
|
@@ -104074,7 +104393,7 @@ function Timeout(delay2) {
|
|
|
104074
104393
|
};
|
|
104075
104394
|
}
|
|
104076
104395
|
// lib/testing/testing-module-builder.ts
|
|
104077
|
-
var
|
|
104396
|
+
var import_reflect_metadata24 = __toESM(require_Reflect(), 1);
|
|
104078
104397
|
|
|
104079
104398
|
// lib/testing/test-app.ts
|
|
104080
104399
|
class TestApp {
|
|
@@ -104229,7 +104548,13 @@ export {
|
|
|
104229
104548
|
SAGA_METADATA,
|
|
104230
104549
|
Request2 as Request,
|
|
104231
104550
|
Render,
|
|
104551
|
+
RedisStorage,
|
|
104552
|
+
RateLimitService,
|
|
104553
|
+
RateLimitExceededException,
|
|
104554
|
+
RateLimit,
|
|
104232
104555
|
RENDER_METADATA,
|
|
104556
|
+
RATELIMIT_METADATA_KEY,
|
|
104557
|
+
RATELIMIT_CONTROLLER_METADATA_KEY,
|
|
104233
104558
|
QueueService,
|
|
104234
104559
|
QueryHandler,
|
|
104235
104560
|
QueryBus,
|
|
@@ -104248,6 +104573,7 @@ export {
|
|
|
104248
104573
|
NoContentResponse,
|
|
104249
104574
|
ModuleInitializationError,
|
|
104250
104575
|
Module,
|
|
104576
|
+
MemoryStorage,
|
|
104251
104577
|
Logger,
|
|
104252
104578
|
LogLevel,
|
|
104253
104579
|
Layout,
|
|
@@ -104274,6 +104600,7 @@ export {
|
|
|
104274
104600
|
DependencyResolutionError,
|
|
104275
104601
|
Delete,
|
|
104276
104602
|
DatabaseError,
|
|
104603
|
+
DEFAULT_RATELIMIT_CONFIG,
|
|
104277
104604
|
Cron,
|
|
104278
104605
|
CreatedResponse,
|
|
104279
104606
|
CqrsModule,
|
|
@@ -10,6 +10,8 @@ export declare class AppStartup {
|
|
|
10
10
|
private static readonly logger;
|
|
11
11
|
private static readonly registeredSagas;
|
|
12
12
|
private static readonly viewBundles;
|
|
13
|
+
private static globalRateLimitConfig;
|
|
14
|
+
private static rateLimitService;
|
|
13
15
|
/**
|
|
14
16
|
* Initializes the application from a root module.
|
|
15
17
|
*
|
|
@@ -68,6 +70,10 @@ export declare class AppStartup {
|
|
|
68
70
|
private static executeControllerMethod;
|
|
69
71
|
private static registerModules;
|
|
70
72
|
private static registerRoutes;
|
|
73
|
+
/**
|
|
74
|
+
* Builds effective rate limit configuration by merging global config with method config
|
|
75
|
+
*/
|
|
76
|
+
private static buildEffectiveRateLimit;
|
|
71
77
|
private static registerTimeouts;
|
|
72
78
|
private static registerCronJobs;
|
|
73
79
|
private static registerBullMqWorkers;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadados utilizados pelo sistema de rate limit
|
|
3
|
+
*/
|
|
4
|
+
/** Chave de metadado para configuração de rate limit em métodos */
|
|
5
|
+
export declare const RATELIMIT_METADATA_KEY = "dip:ratelimit";
|
|
6
|
+
/** Chave de metadado para configuração de rate limit em controllers */
|
|
7
|
+
export declare const RATELIMIT_CONTROLLER_METADATA_KEY = "dip:ratelimit:controller";
|
|
8
|
+
/** Configurações padrão de rate limit */
|
|
9
|
+
export declare const DEFAULT_RATELIMIT_CONFIG: {
|
|
10
|
+
max: number;
|
|
11
|
+
windowMs: number;
|
|
12
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface que define o contrato para storage de rate limiting.
|
|
3
|
+
* Suporta implementações em memória e Redis.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Informações de consumo de rate limit
|
|
7
|
+
*/
|
|
8
|
+
export interface RateLimitInfo {
|
|
9
|
+
/** Total de requisições permitidas no window */
|
|
10
|
+
totalHits: number;
|
|
11
|
+
/** Quantidade de requisições restantes */
|
|
12
|
+
remaining: number;
|
|
13
|
+
/** Timestamp (ms) quando o window reseta */
|
|
14
|
+
resetTime: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Interface base para storage de rate limit
|
|
18
|
+
*/
|
|
19
|
+
export interface RateLimitStorage {
|
|
20
|
+
/**
|
|
21
|
+
* Incrementa o contador para uma chave específica
|
|
22
|
+
* @param key Chave única (IP + endpoint)
|
|
23
|
+
* @param windowMs Janela de tempo em milissegundos
|
|
24
|
+
* @returns Informações atualizadas de rate limit
|
|
25
|
+
*/
|
|
26
|
+
increment(key: string, windowMs: number): Promise<RateLimitInfo> | RateLimitInfo;
|
|
27
|
+
/**
|
|
28
|
+
* Decrementa o contador (útil quando requisição é rejeitada antes do processamento)
|
|
29
|
+
* @param key Chave única
|
|
30
|
+
*/
|
|
31
|
+
decrement(key: string): Promise<void> | void;
|
|
32
|
+
/**
|
|
33
|
+
* Reseta o contador para uma chave específica
|
|
34
|
+
* @param key Chave única
|
|
35
|
+
*/
|
|
36
|
+
resetKey(key: string): Promise<void> | void;
|
|
37
|
+
/**
|
|
38
|
+
* Fecha a conexão (para storage externo como Redis)
|
|
39
|
+
*/
|
|
40
|
+
close?(): Promise<void> | void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import type { HttpRequest } from "../types/http-request";
|
|
3
|
+
import type { RateLimitStorage } from "./interfaces/storage.interface";
|
|
4
|
+
import type { RateLimitConfig } from "./ratelimit.service";
|
|
5
|
+
/**
|
|
6
|
+
* Opções para o decorator @RateLimit
|
|
7
|
+
*/
|
|
8
|
+
export interface RateLimitOptions {
|
|
9
|
+
/** Se o rate limit está habilitado (padrão: true) */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/** Máximo de requisições permitidas na janela de tempo (padrão: 100) */
|
|
12
|
+
max?: number;
|
|
13
|
+
/** Janela de tempo em milissegundos (padrão: 60000 = 1 minuto) */
|
|
14
|
+
windowMs?: number;
|
|
15
|
+
/** Mensagem de erro customizada quando excede o limite */
|
|
16
|
+
message?: string;
|
|
17
|
+
/** Storage customizado (padrão: MemoryStorage) */
|
|
18
|
+
storage?: RateLimitStorage;
|
|
19
|
+
/** Função para extrair a chave de identificação */
|
|
20
|
+
keyGenerator?: (req: HttpRequest) => string;
|
|
21
|
+
/** Header que permite pular o rate limit */
|
|
22
|
+
skipHeader?: string;
|
|
23
|
+
/** Função que determina se deve pular o rate limit */
|
|
24
|
+
skip?: (req: HttpRequest) => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Configuração interna completa de rate limit
|
|
28
|
+
*/
|
|
29
|
+
export interface RateLimitMetadata extends RateLimitConfig {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decorator para aplicar rate limiting em métodos de controller ou em todo o controller.
|
|
34
|
+
*
|
|
35
|
+
* @param options Opções de configuração do rate limit
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // No controller (afeta todos os métodos)
|
|
40
|
+
* @RateLimit({ max: 50, windowMs: 60000 })
|
|
41
|
+
* @Controller('api')
|
|
42
|
+
* class MyController {
|
|
43
|
+
* // No método (sobrescreve o do controller)
|
|
44
|
+
* @Get('data')
|
|
45
|
+
* @RateLimit({ max: 100, windowMs: 60000 })
|
|
46
|
+
* getData() {
|
|
47
|
+
* return { data: [] };
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function RateLimit(options?: RateLimitOptions): any;
|