nestjs-ddd-cli 2.2.0 → 3.2.1

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 (254) hide show
  1. package/README.md +247 -408
  2. package/ddd.schema.json +111 -0
  3. package/dist/commands/aggregate-validator.d.ts +9 -0
  4. package/dist/commands/aggregate-validator.js +953 -0
  5. package/dist/commands/aggregate-validator.js.map +1 -0
  6. package/dist/commands/ai-assist.d.ts +8 -0
  7. package/dist/commands/ai-assist.js +337 -0
  8. package/dist/commands/ai-assist.js.map +1 -0
  9. package/dist/commands/api-contracts.d.ts +9 -0
  10. package/dist/commands/api-contracts.js +1368 -0
  11. package/dist/commands/api-contracts.js.map +1 -0
  12. package/dist/commands/api-docs.d.ts +8 -0
  13. package/dist/commands/api-docs.js +408 -0
  14. package/dist/commands/api-docs.js.map +1 -0
  15. package/dist/commands/api-versioning.d.ts +11 -0
  16. package/dist/commands/api-versioning.js +643 -0
  17. package/dist/commands/api-versioning.js.map +1 -0
  18. package/dist/commands/audit-logging.d.ts +9 -0
  19. package/dist/commands/audit-logging.js +1129 -0
  20. package/dist/commands/audit-logging.js.map +1 -0
  21. package/dist/commands/batch-generate.d.ts +10 -0
  22. package/dist/commands/batch-generate.js +405 -0
  23. package/dist/commands/batch-generate.js.map +1 -0
  24. package/dist/commands/caching-strategies.d.ts +9 -0
  25. package/dist/commands/caching-strategies.js +874 -0
  26. package/dist/commands/caching-strategies.js.map +1 -0
  27. package/dist/commands/code-analyzer.d.ts +42 -0
  28. package/dist/commands/code-analyzer.js +474 -0
  29. package/dist/commands/code-analyzer.js.map +1 -0
  30. package/dist/commands/database-seeding.d.ts +6 -0
  31. package/dist/commands/database-seeding.js +621 -0
  32. package/dist/commands/database-seeding.js.map +1 -0
  33. package/dist/commands/db-optimization.d.ts +7 -0
  34. package/dist/commands/db-optimization.js +687 -0
  35. package/dist/commands/db-optimization.js.map +1 -0
  36. package/dist/commands/dependency-graph.d.ts +6 -0
  37. package/dist/commands/dependency-graph.js +329 -0
  38. package/dist/commands/dependency-graph.js.map +1 -0
  39. package/dist/commands/doctor-enhanced.d.ts +22 -0
  40. package/dist/commands/doctor-enhanced.js +543 -0
  41. package/dist/commands/doctor-enhanced.js.map +1 -0
  42. package/dist/commands/doctor.d.ts +4 -0
  43. package/dist/commands/doctor.js +151 -0
  44. package/dist/commands/doctor.js.map +1 -0
  45. package/dist/commands/env-manager.d.ts +6 -0
  46. package/dist/commands/env-manager.js +419 -0
  47. package/dist/commands/env-manager.js.map +1 -0
  48. package/dist/commands/event-sourcing-full.d.ts +10 -0
  49. package/dist/commands/event-sourcing-full.js +1107 -0
  50. package/dist/commands/event-sourcing-full.js.map +1 -0
  51. package/dist/commands/feature-flags.d.ts +9 -0
  52. package/dist/commands/feature-flags.js +824 -0
  53. package/dist/commands/feature-flags.js.map +1 -0
  54. package/dist/commands/filter-dsl.d.ts +10 -0
  55. package/dist/commands/filter-dsl.js +1407 -0
  56. package/dist/commands/filter-dsl.js.map +1 -0
  57. package/dist/commands/generate-all.js +485 -32
  58. package/dist/commands/generate-all.js.map +1 -1
  59. package/dist/commands/generate-deployment.d.ts +8 -0
  60. package/dist/commands/generate-deployment.js +746 -0
  61. package/dist/commands/generate-deployment.js.map +1 -0
  62. package/dist/commands/generate-domain-service.d.ts +14 -0
  63. package/dist/commands/generate-domain-service.js +796 -0
  64. package/dist/commands/generate-domain-service.js.map +1 -0
  65. package/dist/commands/generate-entity.js +82 -24
  66. package/dist/commands/generate-entity.js.map +1 -1
  67. package/dist/commands/generate-from-schema.d.ts +56 -0
  68. package/dist/commands/generate-from-schema.js +222 -0
  69. package/dist/commands/generate-from-schema.js.map +1 -0
  70. package/dist/commands/generate-orchestrator.d.ts +14 -0
  71. package/dist/commands/generate-orchestrator.js +887 -0
  72. package/dist/commands/generate-orchestrator.js.map +1 -0
  73. package/dist/commands/generate-repository.d.ts +14 -0
  74. package/dist/commands/generate-repository.js +1019 -0
  75. package/dist/commands/generate-repository.js.map +1 -0
  76. package/dist/commands/generate-shared.d.ts +4 -0
  77. package/dist/commands/generate-shared.js +388 -0
  78. package/dist/commands/generate-shared.js.map +1 -0
  79. package/dist/commands/generate-value-object.d.ts +32 -0
  80. package/dist/commands/generate-value-object.js +700 -0
  81. package/dist/commands/generate-value-object.js.map +1 -0
  82. package/dist/commands/graphql-subscriptions.d.ts +6 -0
  83. package/dist/commands/graphql-subscriptions.js +607 -0
  84. package/dist/commands/graphql-subscriptions.js.map +1 -0
  85. package/dist/commands/graphql-types.d.ts +5 -0
  86. package/dist/commands/graphql-types.js +423 -0
  87. package/dist/commands/graphql-types.js.map +1 -0
  88. package/dist/commands/health-probes-advanced.d.ts +6 -0
  89. package/dist/commands/health-probes-advanced.js +655 -0
  90. package/dist/commands/health-probes-advanced.js.map +1 -0
  91. package/dist/commands/i18n-setup.d.ts +10 -0
  92. package/dist/commands/i18n-setup.js +677 -0
  93. package/dist/commands/i18n-setup.js.map +1 -0
  94. package/dist/commands/init-config.d.ts +6 -0
  95. package/dist/commands/init-config.js +370 -0
  96. package/dist/commands/init-config.js.map +1 -0
  97. package/dist/commands/init-project.js +56 -6
  98. package/dist/commands/init-project.js.map +1 -1
  99. package/dist/commands/interactive-scaffold.d.ts +5 -0
  100. package/dist/commands/interactive-scaffold.js +271 -0
  101. package/dist/commands/interactive-scaffold.js.map +1 -0
  102. package/dist/commands/metrics-prometheus.d.ts +6 -0
  103. package/dist/commands/metrics-prometheus.js +681 -0
  104. package/dist/commands/metrics-prometheus.js.map +1 -0
  105. package/dist/commands/migration-engine.d.ts +6 -0
  106. package/dist/commands/migration-engine.js +446 -0
  107. package/dist/commands/migration-engine.js.map +1 -0
  108. package/dist/commands/migration.d.ts +12 -0
  109. package/dist/commands/migration.js +484 -0
  110. package/dist/commands/migration.js.map +1 -0
  111. package/dist/commands/monorepo.d.ts +8 -0
  112. package/dist/commands/monorepo.js +483 -0
  113. package/dist/commands/monorepo.js.map +1 -0
  114. package/dist/commands/multi-database.d.ts +5 -0
  115. package/dist/commands/multi-database.js +439 -0
  116. package/dist/commands/multi-database.js.map +1 -0
  117. package/dist/commands/observability-tracing.d.ts +10 -0
  118. package/dist/commands/observability-tracing.js +740 -0
  119. package/dist/commands/observability-tracing.js.map +1 -0
  120. package/dist/commands/openapi-export.d.ts +8 -0
  121. package/dist/commands/openapi-export.js +359 -0
  122. package/dist/commands/openapi-export.js.map +1 -0
  123. package/dist/commands/perf-analyzer.d.ts +8 -0
  124. package/dist/commands/perf-analyzer.js +423 -0
  125. package/dist/commands/perf-analyzer.js.map +1 -0
  126. package/dist/commands/rate-limiting.d.ts +10 -0
  127. package/dist/commands/rate-limiting.js +953 -0
  128. package/dist/commands/rate-limiting.js.map +1 -0
  129. package/dist/commands/recipe-plugin.d.ts +56 -0
  130. package/dist/commands/recipe-plugin.js +315 -0
  131. package/dist/commands/recipe-plugin.js.map +1 -0
  132. package/dist/commands/recipe.d.ts +6 -0
  133. package/dist/commands/recipe.js +3941 -0
  134. package/dist/commands/recipe.js.map +1 -0
  135. package/dist/commands/recipes/elasticsearch.recipe.d.ts +1 -0
  136. package/dist/commands/recipes/elasticsearch.recipe.js +761 -0
  137. package/dist/commands/recipes/elasticsearch.recipe.js.map +1 -0
  138. package/dist/commands/recipes/event-sourcing.recipe.d.ts +1 -0
  139. package/dist/commands/recipes/event-sourcing.recipe.js +889 -0
  140. package/dist/commands/recipes/event-sourcing.recipe.js.map +1 -0
  141. package/dist/commands/recipes/index.d.ts +7 -0
  142. package/dist/commands/recipes/index.js +24 -0
  143. package/dist/commands/recipes/index.js.map +1 -0
  144. package/dist/commands/recipes/message-queue.recipe.d.ts +1 -0
  145. package/dist/commands/recipes/message-queue.recipe.js +706 -0
  146. package/dist/commands/recipes/message-queue.recipe.js.map +1 -0
  147. package/dist/commands/recipes/middleware.recipe.d.ts +1 -0
  148. package/dist/commands/recipes/middleware.recipe.js +383 -0
  149. package/dist/commands/recipes/middleware.recipe.js.map +1 -0
  150. package/dist/commands/recipes/multi-tenancy.recipe.d.ts +1 -0
  151. package/dist/commands/recipes/multi-tenancy.recipe.js +520 -0
  152. package/dist/commands/recipes/multi-tenancy.recipe.js.map +1 -0
  153. package/dist/commands/recipes/oauth2.recipe.d.ts +1 -0
  154. package/dist/commands/recipes/oauth2.recipe.js +472 -0
  155. package/dist/commands/recipes/oauth2.recipe.js.map +1 -0
  156. package/dist/commands/recipes/websocket.recipe.d.ts +1 -0
  157. package/dist/commands/recipes/websocket.recipe.js +453 -0
  158. package/dist/commands/recipes/websocket.recipe.js.map +1 -0
  159. package/dist/commands/resilience-patterns.d.ts +13 -0
  160. package/dist/commands/resilience-patterns.js +1029 -0
  161. package/dist/commands/resilience-patterns.js.map +1 -0
  162. package/dist/commands/security-patterns.d.ts +11 -0
  163. package/dist/commands/security-patterns.js +2233 -0
  164. package/dist/commands/security-patterns.js.map +1 -0
  165. package/dist/commands/template-debug.d.ts +27 -0
  166. package/dist/commands/template-debug.js +388 -0
  167. package/dist/commands/template-debug.js.map +1 -0
  168. package/dist/commands/test-factory-full.d.ts +9 -0
  169. package/dist/commands/test-factory-full.js +1570 -0
  170. package/dist/commands/test-factory-full.js.map +1 -0
  171. package/dist/commands/test-scaffold.d.ts +7 -0
  172. package/dist/commands/test-scaffold.js +621 -0
  173. package/dist/commands/test-scaffold.js.map +1 -0
  174. package/dist/index.js +1088 -0
  175. package/dist/index.js.map +1 -1
  176. package/dist/templates/ai-context/CLAUDE.md.hbs +158 -0
  177. package/dist/templates/ai-context/conventions.md.hbs +154 -0
  178. package/dist/templates/command/create-command.hbs +6 -14
  179. package/dist/templates/command/delete-command.hbs +19 -0
  180. package/dist/templates/command/update-command.hbs +24 -0
  181. package/dist/templates/controller/controller.hbs +64 -17
  182. package/dist/templates/dto/create-dto.hbs +29 -5
  183. package/dist/templates/dto/filter-dto.hbs +52 -0
  184. package/dist/templates/dto/filter-query.dto.hbs +148 -0
  185. package/dist/templates/dto/paginated-response.dto.hbs +29 -0
  186. package/dist/templates/dto/pagination-query.dto.hbs +30 -0
  187. package/dist/templates/dto/response-dto.hbs +38 -0
  188. package/dist/templates/dto/update-dto.hbs +11 -0
  189. package/dist/templates/entity/entity.hbs +32 -1
  190. package/dist/templates/event/domain-event.hbs +33 -7
  191. package/dist/templates/event/event-handler.hbs +40 -0
  192. package/dist/templates/exception/base-exceptions.hbs +69 -0
  193. package/dist/templates/exception/entity-not-found.exception.hbs +7 -0
  194. package/dist/templates/mapper/mapper.hbs +49 -24
  195. package/dist/templates/module/module.hbs +34 -10
  196. package/dist/templates/orm-entity/orm-entity.hbs +63 -12
  197. package/dist/templates/prisma/prisma-mapper.hbs +71 -0
  198. package/dist/templates/prisma/prisma-repository.hbs +114 -0
  199. package/dist/templates/prisma/prisma-schema.hbs +20 -0
  200. package/dist/templates/prisma/prisma-service.hbs +51 -0
  201. package/dist/templates/query/get-all.query.hbs +50 -0
  202. package/dist/templates/query/get-by-id.query.hbs +31 -0
  203. package/dist/templates/repository/repository.hbs +55 -13
  204. package/dist/templates/resolver/graphql-input.hbs +54 -0
  205. package/dist/templates/resolver/graphql-type.hbs +58 -0
  206. package/dist/templates/resolver/pagination-args.hbs +33 -0
  207. package/dist/templates/resolver/resolver.hbs +62 -0
  208. package/dist/templates/shared/prisma-query-builder.util.hbs +189 -0
  209. package/dist/templates/shared/query-builder.util.hbs +218 -0
  210. package/dist/templates/test/controller.spec.hbs +124 -0
  211. package/dist/templates/test/repository.spec.hbs +158 -0
  212. package/dist/templates/test/usecase.spec.hbs +116 -0
  213. package/dist/templates/usecase/create-usecase.hbs +19 -7
  214. package/dist/templates/usecase/delete-usecase.hbs +17 -0
  215. package/dist/templates/usecase/update-usecase.hbs +31 -0
  216. package/dist/utils/config.utils.d.ts +45 -0
  217. package/dist/utils/config.utils.js +211 -0
  218. package/dist/utils/config.utils.js.map +1 -0
  219. package/dist/utils/error.utils.d.ts +145 -0
  220. package/dist/utils/error.utils.js +422 -0
  221. package/dist/utils/error.utils.js.map +1 -0
  222. package/dist/utils/field.utils.d.ts +54 -0
  223. package/dist/utils/field.utils.js +389 -0
  224. package/dist/utils/field.utils.js.map +1 -0
  225. package/dist/utils/file.utils.d.ts +19 -8
  226. package/dist/utils/file.utils.js +135 -4
  227. package/dist/utils/file.utils.js.map +1 -1
  228. package/dist/utils/idempotency.utils.d.ts +123 -0
  229. package/dist/utils/idempotency.utils.js +444 -0
  230. package/dist/utils/idempotency.utils.js.map +1 -0
  231. package/dist/utils/naming.utils.js +26 -3
  232. package/dist/utils/naming.utils.js.map +1 -1
  233. package/dist/utils/performance.utils.d.ts +37 -0
  234. package/dist/utils/performance.utils.js +158 -0
  235. package/dist/utils/performance.utils.js.map +1 -0
  236. package/dist/utils/relation.utils.d.ts +92 -0
  237. package/dist/utils/relation.utils.js +388 -0
  238. package/dist/utils/relation.utils.js.map +1 -0
  239. package/dist/utils/rollback.utils.d.ts +49 -0
  240. package/dist/utils/rollback.utils.js +306 -0
  241. package/dist/utils/rollback.utils.js.map +1 -0
  242. package/dist/utils/schema.utils.d.ts +123 -0
  243. package/dist/utils/schema.utils.js +419 -0
  244. package/dist/utils/schema.utils.js.map +1 -0
  245. package/dist/utils/security.utils.d.ts +57 -0
  246. package/dist/utils/security.utils.js +315 -0
  247. package/dist/utils/security.utils.js.map +1 -0
  248. package/dist/utils/template-engine.utils.d.ts +80 -0
  249. package/dist/utils/template-engine.utils.js +463 -0
  250. package/dist/utils/template-engine.utils.js.map +1 -0
  251. package/dist/utils/validation-registry.utils.d.ts +160 -0
  252. package/dist/utils/validation-registry.utils.js +526 -0
  253. package/dist/utils/validation-registry.utils.js.map +1 -0
  254. package/package.json +3 -1
@@ -0,0 +1,953 @@
1
+ "use strict";
2
+ /**
3
+ * Rate Limiting & Throttling Framework Generator
4
+ * Generates comprehensive rate limiting infrastructure
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.setupRateLimiting = setupRateLimiting;
44
+ const fs = __importStar(require("fs"));
45
+ const path = __importStar(require("path"));
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ async function setupRateLimiting(basePath, options = {}) {
48
+ console.log(chalk_1.default.bold.blue('\n⏱️ Setting up Rate Limiting Framework\n'));
49
+ const sharedPath = path.join(basePath, 'src/shared/rate-limiting');
50
+ if (!fs.existsSync(sharedPath)) {
51
+ fs.mkdirSync(sharedPath, { recursive: true });
52
+ }
53
+ // Generate rate limiting module
54
+ const moduleContent = generateRateLimitingModule(options);
55
+ fs.writeFileSync(path.join(sharedPath, 'rate-limiting.module.ts'), moduleContent);
56
+ console.log(chalk_1.default.green(` ✓ Created rate limiting module`));
57
+ // Generate rate limiter service
58
+ const serviceContent = generateRateLimiterService(options);
59
+ fs.writeFileSync(path.join(sharedPath, 'rate-limiter.service.ts'), serviceContent);
60
+ console.log(chalk_1.default.green(` ✓ Created rate limiter service`));
61
+ // Generate rate limit guard
62
+ const guardContent = generateRateLimitGuard();
63
+ fs.writeFileSync(path.join(sharedPath, 'rate-limit.guard.ts'), guardContent);
64
+ console.log(chalk_1.default.green(` ✓ Created rate limit guard`));
65
+ // Generate throttle decorator
66
+ const decoratorContent = generateThrottleDecorator();
67
+ fs.writeFileSync(path.join(sharedPath, 'throttle.decorator.ts'), decoratorContent);
68
+ console.log(chalk_1.default.green(` ✓ Created throttle decorator`));
69
+ // Generate quota manager
70
+ const quotaContent = generateQuotaManager();
71
+ fs.writeFileSync(path.join(sharedPath, 'quota-manager.ts'), quotaContent);
72
+ console.log(chalk_1.default.green(` ✓ Created quota manager`));
73
+ // Generate metrics collector
74
+ const metricsContent = generateMetricsCollector();
75
+ fs.writeFileSync(path.join(sharedPath, 'metrics.collector.ts'), metricsContent);
76
+ console.log(chalk_1.default.green(` ✓ Created metrics collector`));
77
+ console.log(chalk_1.default.bold.green('\n✅ Rate limiting framework ready!\n'));
78
+ }
79
+ function generateRateLimitingModule(options) {
80
+ return `import { Module, Global, DynamicModule } from '@nestjs/common';
81
+ import { APP_GUARD } from '@nestjs/core';
82
+ import { RateLimiterService } from './rate-limiter.service';
83
+ import { RateLimitGuard } from './rate-limit.guard';
84
+ import { QuotaManager } from './quota-manager';
85
+ import { MetricsCollector } from './metrics.collector';
86
+
87
+ export interface RateLimitingModuleOptions {
88
+ strategy: 'token-bucket' | 'sliding-window' | 'fixed-window';
89
+ storage: 'memory' | 'redis';
90
+ redis?: {
91
+ host: string;
92
+ port: number;
93
+ password?: string;
94
+ };
95
+ defaults: {
96
+ ttl: number;
97
+ limit: number;
98
+ };
99
+ skipIf?: (context: any) => boolean;
100
+ keyGenerator?: (context: any) => string;
101
+ }
102
+
103
+ @Global()
104
+ @Module({})
105
+ export class RateLimitingModule {
106
+ static forRoot(options: RateLimitingModuleOptions): DynamicModule {
107
+ return {
108
+ module: RateLimitingModule,
109
+ providers: [
110
+ {
111
+ provide: 'RATE_LIMITING_OPTIONS',
112
+ useValue: options,
113
+ },
114
+ RateLimiterService,
115
+ QuotaManager,
116
+ MetricsCollector,
117
+ {
118
+ provide: APP_GUARD,
119
+ useClass: RateLimitGuard,
120
+ },
121
+ ],
122
+ exports: [RateLimiterService, QuotaManager, MetricsCollector],
123
+ };
124
+ }
125
+
126
+ static forFeature(config: FeatureRateLimitConfig): DynamicModule {
127
+ return {
128
+ module: RateLimitingModule,
129
+ providers: [
130
+ {
131
+ provide: 'FEATURE_RATE_LIMIT_CONFIG',
132
+ useValue: config,
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ }
138
+
139
+ export interface FeatureRateLimitConfig {
140
+ name: string;
141
+ ttl: number;
142
+ limit: number;
143
+ keyPrefix?: string;
144
+ }
145
+ `;
146
+ }
147
+ function generateRateLimiterService(options) {
148
+ const strategy = options.strategy || 'sliding-window';
149
+ return `import { Injectable, Inject, Logger } from '@nestjs/common';
150
+ import { MetricsCollector } from './metrics.collector';
151
+
152
+ export interface RateLimitResult {
153
+ allowed: boolean;
154
+ remaining: number;
155
+ resetAt: Date;
156
+ retryAfter?: number;
157
+ }
158
+
159
+ export interface RateLimitConfig {
160
+ key: string;
161
+ limit: number;
162
+ ttl: number; // in seconds
163
+ }
164
+
165
+ @Injectable()
166
+ export class RateLimiterService {
167
+ private readonly logger = new Logger(RateLimiterService.name);
168
+ private readonly storage = new Map<string, RateLimitEntry>();
169
+
170
+ constructor(
171
+ @Inject('RATE_LIMITING_OPTIONS') private readonly options: any,
172
+ private readonly metrics: MetricsCollector,
173
+ ) {}
174
+
175
+ /**
176
+ * Check if request should be allowed
177
+ */
178
+ async check(config: RateLimitConfig): Promise<RateLimitResult> {
179
+ const now = Date.now();
180
+ const windowStart = now - (config.ttl * 1000);
181
+
182
+ let entry = this.storage.get(config.key);
183
+
184
+ // Clean old entries
185
+ if (entry) {
186
+ entry.requests = entry.requests.filter(time => time > windowStart);
187
+ }
188
+
189
+ if (!entry) {
190
+ entry = { requests: [], createdAt: now };
191
+ this.storage.set(config.key, entry);
192
+ }
193
+
194
+ const currentCount = entry.requests.length;
195
+ const remaining = Math.max(0, config.limit - currentCount);
196
+ const resetAt = new Date(windowStart + (config.ttl * 1000));
197
+
198
+ if (currentCount >= config.limit) {
199
+ const retryAfter = Math.ceil((entry.requests[0] + (config.ttl * 1000) - now) / 1000);
200
+
201
+ this.metrics.recordRateLimitHit(config.key, false);
202
+
203
+ return {
204
+ allowed: false,
205
+ remaining: 0,
206
+ resetAt,
207
+ retryAfter: Math.max(1, retryAfter),
208
+ };
209
+ }
210
+
211
+ // Record request
212
+ entry.requests.push(now);
213
+ this.metrics.recordRateLimitHit(config.key, true);
214
+
215
+ return {
216
+ allowed: true,
217
+ remaining: remaining - 1,
218
+ resetAt,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Token bucket algorithm
224
+ */
225
+ async checkTokenBucket(config: RateLimitConfig & { refillRate: number }): Promise<RateLimitResult> {
226
+ const now = Date.now();
227
+ let entry = this.storage.get(config.key) as TokenBucketEntry | undefined;
228
+
229
+ if (!entry) {
230
+ entry = {
231
+ tokens: config.limit,
232
+ lastRefill: now,
233
+ requests: [],
234
+ createdAt: now,
235
+ };
236
+ this.storage.set(config.key, entry);
237
+ }
238
+
239
+ // Refill tokens
240
+ const timePassed = (now - entry.lastRefill) / 1000;
241
+ const tokensToAdd = Math.floor(timePassed * config.refillRate);
242
+
243
+ if (tokensToAdd > 0) {
244
+ entry.tokens = Math.min(config.limit, entry.tokens + tokensToAdd);
245
+ entry.lastRefill = now;
246
+ }
247
+
248
+ if (entry.tokens < 1) {
249
+ const timeToNextToken = Math.ceil((1 - entry.tokens) / config.refillRate);
250
+
251
+ return {
252
+ allowed: false,
253
+ remaining: 0,
254
+ resetAt: new Date(now + timeToNextToken * 1000),
255
+ retryAfter: timeToNextToken,
256
+ };
257
+ }
258
+
259
+ entry.tokens--;
260
+
261
+ return {
262
+ allowed: true,
263
+ remaining: Math.floor(entry.tokens),
264
+ resetAt: new Date(now + config.ttl * 1000),
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Reset rate limit for a key
270
+ */
271
+ async reset(key: string): Promise<void> {
272
+ // Validate key to prevent injection
273
+ const sanitizedKey = key.replace(/[^a-zA-Z0-9:_\\-@.]/g, '').substring(0, 256);
274
+ this.storage.delete(sanitizedKey);
275
+ this.logger.debug(\`Reset rate limit for: \${sanitizedKey}\`);
276
+ }
277
+
278
+ /**
279
+ * Block an IP address temporarily (brute force protection)
280
+ */
281
+ async blockIp(ip: string, durationSeconds: number = 3600): Promise<void> {
282
+ const sanitizedIp = this.sanitizeIp(ip);
283
+ if (!sanitizedIp) {
284
+ this.logger.warn(\`Invalid IP address format: \${ip}\`);
285
+ return;
286
+ }
287
+
288
+ const blockKey = \`blocked_ip:\${sanitizedIp}\`;
289
+ this.storage.set(blockKey, {
290
+ requests: [],
291
+ createdAt: Date.now(),
292
+ blockedUntil: Date.now() + (durationSeconds * 1000),
293
+ } as BlockedEntry);
294
+
295
+ this.logger.warn(\`IP blocked for \${durationSeconds}s: \${sanitizedIp}\`);
296
+ this.metrics.recordIpBlock(sanitizedIp);
297
+ }
298
+
299
+ /**
300
+ * Check if an IP is blocked
301
+ */
302
+ async isIpBlocked(ip: string): Promise<boolean> {
303
+ const sanitizedIp = this.sanitizeIp(ip);
304
+ if (!sanitizedIp) return false;
305
+
306
+ const blockKey = \`blocked_ip:\${sanitizedIp}\`;
307
+ const entry = this.storage.get(blockKey) as BlockedEntry | undefined;
308
+
309
+ if (entry && entry.blockedUntil && entry.blockedUntil > Date.now()) {
310
+ return true;
311
+ }
312
+
313
+ // Clean up expired block
314
+ if (entry) {
315
+ this.storage.delete(blockKey);
316
+ }
317
+
318
+ return false;
319
+ }
320
+
321
+ /**
322
+ * Detect brute force and auto-block
323
+ */
324
+ async detectBruteForce(
325
+ ip: string,
326
+ endpoint: string,
327
+ config: { threshold: number; windowSeconds: number; blockDurationSeconds: number }
328
+ ): Promise<boolean> {
329
+ const sanitizedIp = this.sanitizeIp(ip);
330
+ if (!sanitizedIp) return false;
331
+
332
+ const key = \`brute_force:\${sanitizedIp}:\${endpoint}\`;
333
+ const result = await this.check({
334
+ key,
335
+ limit: config.threshold,
336
+ ttl: config.windowSeconds,
337
+ });
338
+
339
+ if (!result.allowed) {
340
+ await this.blockIp(sanitizedIp, config.blockDurationSeconds);
341
+ this.logger.warn(\`Brute force detected from \${sanitizedIp} on \${endpoint}\`);
342
+ return true;
343
+ }
344
+
345
+ return false;
346
+ }
347
+
348
+ /**
349
+ * Sanitize and validate IP address
350
+ */
351
+ private sanitizeIp(ip: string): string | null {
352
+ if (!ip || typeof ip !== 'string') return null;
353
+
354
+ // Remove any port suffix
355
+ const cleanIp = ip.split(':')[0].trim();
356
+
357
+ // Basic IPv4 validation
358
+ const ipv4Regex = /^(\\d{1,3}\\.){3}\\d{1,3}$/;
359
+ // Basic IPv6 validation (simplified)
360
+ const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
361
+
362
+ if (ipv4Regex.test(cleanIp) || ipv6Regex.test(cleanIp)) {
363
+ return cleanIp;
364
+ }
365
+
366
+ return null;
367
+ }
368
+
369
+ /**
370
+ * Get current status for a key
371
+ */
372
+ async getStatus(key: string, config: RateLimitConfig): Promise<RateLimitStatus> {
373
+ const entry = this.storage.get(key);
374
+ const now = Date.now();
375
+ const windowStart = now - (config.ttl * 1000);
376
+
377
+ if (!entry) {
378
+ return {
379
+ key,
380
+ current: 0,
381
+ limit: config.limit,
382
+ remaining: config.limit,
383
+ resetAt: new Date(now + config.ttl * 1000),
384
+ };
385
+ }
386
+
387
+ const validRequests = entry.requests.filter(time => time > windowStart);
388
+
389
+ return {
390
+ key,
391
+ current: validRequests.length,
392
+ limit: config.limit,
393
+ remaining: Math.max(0, config.limit - validRequests.length),
394
+ resetAt: new Date(windowStart + (config.ttl * 1000)),
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Cleanup expired entries
400
+ */
401
+ cleanup(): void {
402
+ const now = Date.now();
403
+ const maxAge = 3600000; // 1 hour
404
+
405
+ for (const [key, entry] of this.storage.entries()) {
406
+ if (now - entry.createdAt > maxAge && entry.requests.length === 0) {
407
+ this.storage.delete(key);
408
+ }
409
+ }
410
+ }
411
+ }
412
+
413
+ interface RateLimitEntry {
414
+ requests: number[];
415
+ createdAt: number;
416
+ }
417
+
418
+ interface TokenBucketEntry extends RateLimitEntry {
419
+ tokens: number;
420
+ lastRefill: number;
421
+ }
422
+
423
+ interface BlockedEntry extends RateLimitEntry {
424
+ blockedUntil?: number;
425
+ }
426
+
427
+ export interface RateLimitStatus {
428
+ key: string;
429
+ current: number;
430
+ limit: number;
431
+ remaining: number;
432
+ resetAt: Date;
433
+ }
434
+ `;
435
+ }
436
+ function generateRateLimitGuard() {
437
+ return `import {
438
+ Injectable,
439
+ CanActivate,
440
+ ExecutionContext,
441
+ HttpException,
442
+ HttpStatus,
443
+ Inject,
444
+ } from '@nestjs/common';
445
+ import { Reflector } from '@nestjs/core';
446
+ import { RateLimiterService } from './rate-limiter.service';
447
+ import { MetricsCollector } from './metrics.collector';
448
+
449
+ @Injectable()
450
+ export class RateLimitGuard implements CanActivate {
451
+ constructor(
452
+ private readonly reflector: Reflector,
453
+ private readonly rateLimiter: RateLimiterService,
454
+ private readonly metrics: MetricsCollector,
455
+ @Inject('RATE_LIMITING_OPTIONS') private readonly options: any,
456
+ ) {}
457
+
458
+ async canActivate(context: ExecutionContext): Promise<boolean> {
459
+ // Check if rate limiting is disabled for this route
460
+ const skipRateLimit = this.reflector.get<boolean>('skipRateLimit', context.getHandler());
461
+ if (skipRateLimit) {
462
+ return true;
463
+ }
464
+
465
+ const request = context.switchToHttp().getRequest();
466
+
467
+ // Check if IP is blocked (brute force protection)
468
+ const ip = this.extractIp(request);
469
+ if (ip && await this.rateLimiter.isIpBlocked(ip)) {
470
+ throw new HttpException(
471
+ {
472
+ statusCode: HttpStatus.FORBIDDEN,
473
+ message: 'IP address temporarily blocked',
474
+ },
475
+ HttpStatus.FORBIDDEN,
476
+ );
477
+ }
478
+
479
+ // Check custom skip condition
480
+ if (this.options.skipIf && this.options.skipIf(context)) {
481
+ return true;
482
+ }
483
+
484
+ // Get rate limit config from decorator or use defaults
485
+ const config = this.reflector.get<{ limit: number; ttl: number }>(
486
+ 'rateLimit',
487
+ context.getHandler(),
488
+ ) || this.options.defaults;
489
+
490
+ // Generate key
491
+ const key = this.generateKey(context);
492
+
493
+ // Check rate limit
494
+ const result = await this.rateLimiter.check({
495
+ key,
496
+ limit: config.limit,
497
+ ttl: config.ttl,
498
+ });
499
+
500
+ // Set headers
501
+ const response = context.switchToHttp().getResponse();
502
+ response.setHeader('X-RateLimit-Limit', config.limit);
503
+ response.setHeader('X-RateLimit-Remaining', result.remaining);
504
+ response.setHeader('X-RateLimit-Reset', result.resetAt.toISOString());
505
+
506
+ if (!result.allowed) {
507
+ response.setHeader('Retry-After', result.retryAfter);
508
+
509
+ throw new HttpException(
510
+ {
511
+ statusCode: HttpStatus.TOO_MANY_REQUESTS,
512
+ message: 'Too many requests',
513
+ retryAfter: result.retryAfter,
514
+ },
515
+ HttpStatus.TOO_MANY_REQUESTS,
516
+ );
517
+ }
518
+
519
+ return true;
520
+ }
521
+
522
+ private generateKey(context: ExecutionContext): string {
523
+ if (this.options.keyGenerator) {
524
+ return this.options.keyGenerator(context);
525
+ }
526
+
527
+ const request = context.switchToHttp().getRequest();
528
+ const ip = this.extractIp(request);
529
+ const userId = request.user?.id;
530
+ const path = request.path;
531
+ const method = request.method;
532
+
533
+ // Use user ID if authenticated, otherwise IP
534
+ const identifier = userId || ip || 'unknown';
535
+
536
+ // Sanitize key components
537
+ const sanitizedPath = path.replace(/[^a-zA-Z0-9\\/\\-_]/g, '').substring(0, 100);
538
+ const sanitizedMethod = method.replace(/[^A-Z]/g, '').substring(0, 10);
539
+ const sanitizedIdentifier = identifier.replace(/[^a-zA-Z0-9:@.\\-_]/g, '').substring(0, 128);
540
+
541
+ return \`rate_limit:\${sanitizedIdentifier}:\${sanitizedMethod}:\${sanitizedPath}\`;
542
+ }
543
+
544
+ /**
545
+ * Extract real IP from request (handles proxies)
546
+ */
547
+ private extractIp(request: any): string | null {
548
+ // Check common proxy headers (in order of preference)
549
+ const forwardedFor = request.headers?.['x-forwarded-for'];
550
+ if (forwardedFor) {
551
+ // Take the first IP (client IP) from comma-separated list
552
+ const ips = forwardedFor.split(',').map((ip: string) => ip.trim());
553
+ const clientIp = ips[0];
554
+ // Basic validation
555
+ if (/^[\\d.:a-fA-F]+$/.test(clientIp)) {
556
+ return clientIp;
557
+ }
558
+ }
559
+
560
+ const realIp = request.headers?.['x-real-ip'];
561
+ if (realIp && /^[\\d.:a-fA-F]+$/.test(realIp)) {
562
+ return realIp;
563
+ }
564
+
565
+ // Fallback to direct connection IP
566
+ const ip = request.ip || request.connection?.remoteAddress;
567
+ if (ip && /^[\\d.:a-fA-F]+$/.test(ip)) {
568
+ return ip;
569
+ }
570
+
571
+ return null;
572
+ }
573
+ }
574
+ `;
575
+ }
576
+ function generateThrottleDecorator() {
577
+ return `import { SetMetadata, applyDecorators } from '@nestjs/common';
578
+
579
+ /**
580
+ * Rate limit decorator
581
+ */
582
+ export function RateLimit(options: { limit: number; ttl: number }) {
583
+ return SetMetadata('rateLimit', options);
584
+ }
585
+
586
+ /**
587
+ * Skip rate limiting decorator
588
+ */
589
+ export function SkipRateLimit() {
590
+ return SetMetadata('skipRateLimit', true);
591
+ }
592
+
593
+ /**
594
+ * Throttle decorator (alias for RateLimit)
595
+ */
596
+ export function Throttle(limit: number, ttl: number) {
597
+ return RateLimit({ limit, ttl });
598
+ }
599
+
600
+ /**
601
+ * Throttle by user
602
+ */
603
+ export function ThrottleByUser(limit: number, ttl: number) {
604
+ return applyDecorators(
605
+ SetMetadata('rateLimit', { limit, ttl }),
606
+ SetMetadata('rateLimitKeyType', 'user'),
607
+ );
608
+ }
609
+
610
+ /**
611
+ * Throttle by IP
612
+ */
613
+ export function ThrottleByIP(limit: number, ttl: number) {
614
+ return applyDecorators(
615
+ SetMetadata('rateLimit', { limit, ttl }),
616
+ SetMetadata('rateLimitKeyType', 'ip'),
617
+ );
618
+ }
619
+
620
+ /**
621
+ * Throttle by API key
622
+ */
623
+ export function ThrottleByApiKey(limit: number, ttl: number) {
624
+ return applyDecorators(
625
+ SetMetadata('rateLimit', { limit, ttl }),
626
+ SetMetadata('rateLimitKeyType', 'apiKey'),
627
+ );
628
+ }
629
+
630
+ /**
631
+ * Burst limit decorator
632
+ * Allows bursts up to burstLimit, then throttles
633
+ */
634
+ export function BurstLimit(options: {
635
+ burstLimit: number;
636
+ sustainedLimit: number;
637
+ ttl: number;
638
+ }) {
639
+ return SetMetadata('burstLimit', options);
640
+ }
641
+
642
+ /**
643
+ * Dynamic rate limit
644
+ * Uses a function to determine the limit
645
+ */
646
+ export function DynamicRateLimit(
647
+ limiter: (context: any) => { limit: number; ttl: number },
648
+ ) {
649
+ return SetMetadata('dynamicRateLimit', limiter);
650
+ }
651
+ `;
652
+ }
653
+ function generateQuotaManager() {
654
+ return `import { Injectable, Logger } from '@nestjs/common';
655
+
656
+ export interface QuotaConfig {
657
+ name: string;
658
+ limit: number;
659
+ period: 'hourly' | 'daily' | 'monthly';
660
+ hardLimit?: boolean;
661
+ }
662
+
663
+ export interface QuotaUsage {
664
+ name: string;
665
+ used: number;
666
+ limit: number;
667
+ remaining: number;
668
+ resetAt: Date;
669
+ percentage: number;
670
+ }
671
+
672
+ @Injectable()
673
+ export class QuotaManager {
674
+ private readonly logger = new Logger(QuotaManager.name);
675
+ private readonly quotas = new Map<string, QuotaEntry>();
676
+
677
+ /**
678
+ * Register a quota
679
+ */
680
+ register(userId: string, config: QuotaConfig): void {
681
+ const key = this.getKey(userId, config.name);
682
+ const resetAt = this.calculateResetTime(config.period);
683
+
684
+ this.quotas.set(key, {
685
+ config,
686
+ used: 0,
687
+ resetAt,
688
+ });
689
+ }
690
+
691
+ /**
692
+ * Check and consume quota
693
+ */
694
+ async consume(
695
+ userId: string,
696
+ quotaName: string,
697
+ amount: number = 1,
698
+ ): Promise<{ allowed: boolean; usage: QuotaUsage }> {
699
+ const key = this.getKey(userId, quotaName);
700
+ let entry = this.quotas.get(key);
701
+
702
+ if (!entry) {
703
+ throw new Error(\`Quota '\${quotaName}' not registered for user \${userId}\`);
704
+ }
705
+
706
+ // Reset if period has passed
707
+ if (new Date() > entry.resetAt) {
708
+ entry.used = 0;
709
+ entry.resetAt = this.calculateResetTime(entry.config.period);
710
+ }
711
+
712
+ const wouldExceed = entry.used + amount > entry.config.limit;
713
+
714
+ if (wouldExceed && entry.config.hardLimit) {
715
+ return {
716
+ allowed: false,
717
+ usage: this.getUsage(entry),
718
+ };
719
+ }
720
+
721
+ entry.used += amount;
722
+
723
+ return {
724
+ allowed: !wouldExceed,
725
+ usage: this.getUsage(entry),
726
+ };
727
+ }
728
+
729
+ /**
730
+ * Get quota usage
731
+ */
732
+ getUsage(entry: QuotaEntry): QuotaUsage;
733
+ getUsage(userId: string, quotaName: string): QuotaUsage | null;
734
+ getUsage(userIdOrEntry: string | QuotaEntry, quotaName?: string): QuotaUsage | null {
735
+ if (typeof userIdOrEntry === 'object') {
736
+ const entry = userIdOrEntry;
737
+ return {
738
+ name: entry.config.name,
739
+ used: entry.used,
740
+ limit: entry.config.limit,
741
+ remaining: Math.max(0, entry.config.limit - entry.used),
742
+ resetAt: entry.resetAt,
743
+ percentage: (entry.used / entry.config.limit) * 100,
744
+ };
745
+ }
746
+
747
+ const key = this.getKey(userIdOrEntry, quotaName!);
748
+ const entry = this.quotas.get(key);
749
+ return entry ? this.getUsage(entry) : null;
750
+ }
751
+
752
+ /**
753
+ * Get all quotas for a user
754
+ */
755
+ getAllQuotas(userId: string): QuotaUsage[] {
756
+ const usages: QuotaUsage[] = [];
757
+
758
+ for (const [key, entry] of this.quotas.entries()) {
759
+ if (key.startsWith(\`\${userId}:\`)) {
760
+ usages.push(this.getUsage(entry));
761
+ }
762
+ }
763
+
764
+ return usages;
765
+ }
766
+
767
+ /**
768
+ * Reset quota
769
+ */
770
+ reset(userId: string, quotaName: string): void {
771
+ const key = this.getKey(userId, quotaName);
772
+ const entry = this.quotas.get(key);
773
+
774
+ if (entry) {
775
+ entry.used = 0;
776
+ entry.resetAt = this.calculateResetTime(entry.config.period);
777
+ }
778
+ }
779
+
780
+ private getKey(userId: string, quotaName: string): string {
781
+ return \`\${userId}:\${quotaName}\`;
782
+ }
783
+
784
+ private calculateResetTime(period: 'hourly' | 'daily' | 'monthly'): Date {
785
+ const now = new Date();
786
+
787
+ switch (period) {
788
+ case 'hourly':
789
+ return new Date(now.getTime() + 3600000);
790
+ case 'daily':
791
+ const tomorrow = new Date(now);
792
+ tomorrow.setDate(tomorrow.getDate() + 1);
793
+ tomorrow.setHours(0, 0, 0, 0);
794
+ return tomorrow;
795
+ case 'monthly':
796
+ const nextMonth = new Date(now);
797
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
798
+ nextMonth.setDate(1);
799
+ nextMonth.setHours(0, 0, 0, 0);
800
+ return nextMonth;
801
+ }
802
+ }
803
+ }
804
+
805
+ interface QuotaEntry {
806
+ config: QuotaConfig;
807
+ used: number;
808
+ resetAt: Date;
809
+ }
810
+
811
+ /**
812
+ * Quota decorator
813
+ */
814
+ export function Quota(quotaName: string, amount: number = 1): MethodDecorator {
815
+ return function (
816
+ target: any,
817
+ propertyKey: string | symbol,
818
+ descriptor: PropertyDescriptor,
819
+ ) {
820
+ const original = descriptor.value;
821
+
822
+ descriptor.value = async function (...args: any[]) {
823
+ const quotaManager: QuotaManager = (this as any).quotaManager;
824
+ const userId = args[0]?.user?.id; // Adjust based on your request structure
825
+
826
+ if (quotaManager && userId) {
827
+ const result = await quotaManager.consume(userId, quotaName, amount);
828
+ if (!result.allowed) {
829
+ throw new Error(\`Quota exceeded for \${quotaName}\`);
830
+ }
831
+ }
832
+
833
+ return original.apply(this, args);
834
+ };
835
+
836
+ return descriptor;
837
+ };
838
+ }
839
+ `;
840
+ }
841
+ function generateMetricsCollector() {
842
+ return `import { Injectable, Logger } from '@nestjs/common';
843
+
844
+ export interface RateLimitMetrics {
845
+ totalRequests: number;
846
+ allowedRequests: number;
847
+ blockedRequests: number;
848
+ uniqueKeys: number;
849
+ topBlockedKeys: { key: string; count: number }[];
850
+ requestsPerMinute: number;
851
+ blockRate: number;
852
+ }
853
+
854
+ @Injectable()
855
+ export class MetricsCollector {
856
+ private readonly logger = new Logger(MetricsCollector.name);
857
+ private readonly metrics: MetricEntry[] = [];
858
+ private readonly keyStats = new Map<string, KeyStats>();
859
+ private readonly maxEntries = 10000;
860
+
861
+ /**
862
+ * Record a rate limit check
863
+ */
864
+ recordRateLimitHit(key: string, allowed: boolean): void {
865
+ const now = Date.now();
866
+
867
+ this.metrics.push({
868
+ timestamp: now,
869
+ key,
870
+ allowed,
871
+ });
872
+
873
+ // Update key stats
874
+ const stats = this.keyStats.get(key) || { allowed: 0, blocked: 0 };
875
+ if (allowed) {
876
+ stats.allowed++;
877
+ } else {
878
+ stats.blocked++;
879
+ }
880
+ this.keyStats.set(key, stats);
881
+
882
+ // Cleanup old entries
883
+ this.cleanup();
884
+ }
885
+
886
+ /**
887
+ * Get current metrics
888
+ */
889
+ getMetrics(windowMinutes: number = 5): RateLimitMetrics {
890
+ const windowStart = Date.now() - (windowMinutes * 60 * 1000);
891
+ const recentMetrics = this.metrics.filter(m => m.timestamp > windowStart);
892
+
893
+ const allowed = recentMetrics.filter(m => m.allowed).length;
894
+ const blocked = recentMetrics.filter(m => !m.allowed).length;
895
+ const total = recentMetrics.length;
896
+
897
+ // Get top blocked keys
898
+ const blockedByKey = new Map<string, number>();
899
+ for (const metric of recentMetrics.filter(m => !m.allowed)) {
900
+ blockedByKey.set(metric.key, (blockedByKey.get(metric.key) || 0) + 1);
901
+ }
902
+
903
+ const topBlockedKeys = Array.from(blockedByKey.entries())
904
+ .sort((a, b) => b[1] - a[1])
905
+ .slice(0, 10)
906
+ .map(([key, count]) => ({ key, count }));
907
+
908
+ return {
909
+ totalRequests: total,
910
+ allowedRequests: allowed,
911
+ blockedRequests: blocked,
912
+ uniqueKeys: new Set(recentMetrics.map(m => m.key)).size,
913
+ topBlockedKeys,
914
+ requestsPerMinute: total / windowMinutes,
915
+ blockRate: total > 0 ? (blocked / total) * 100 : 0,
916
+ };
917
+ }
918
+
919
+ /**
920
+ * Get stats for a specific key
921
+ */
922
+ getKeyStats(key: string): KeyStats | null {
923
+ return this.keyStats.get(key) || null;
924
+ }
925
+
926
+ /**
927
+ * Reset metrics
928
+ */
929
+ reset(): void {
930
+ this.metrics.length = 0;
931
+ this.keyStats.clear();
932
+ }
933
+
934
+ private cleanup(): void {
935
+ if (this.metrics.length > this.maxEntries) {
936
+ this.metrics.splice(0, this.metrics.length - this.maxEntries);
937
+ }
938
+ }
939
+ }
940
+
941
+ interface MetricEntry {
942
+ timestamp: number;
943
+ key: string;
944
+ allowed: boolean;
945
+ }
946
+
947
+ interface KeyStats {
948
+ allowed: number;
949
+ blocked: number;
950
+ }
951
+ `;
952
+ }
953
+ //# sourceMappingURL=rate-limiting.js.map