nestjs-shield 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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/dist/algorithms/fixed-window.d.ts +4 -0
  4. package/dist/algorithms/fixed-window.js +9 -0
  5. package/dist/algorithms/leaky-bucket.d.ts +4 -0
  6. package/dist/algorithms/leaky-bucket.js +10 -0
  7. package/dist/algorithms/sliding-window-counter.d.ts +4 -0
  8. package/dist/algorithms/sliding-window-counter.js +9 -0
  9. package/dist/algorithms/sliding-window-log.d.ts +4 -0
  10. package/dist/algorithms/sliding-window-log.js +9 -0
  11. package/dist/algorithms/token-bucket.d.ts +4 -0
  12. package/dist/algorithms/token-bucket.js +10 -0
  13. package/dist/apply-to.d.ts +8 -0
  14. package/dist/apply-to.js +42 -0
  15. package/dist/checks/auto-ban.check.d.ts +6 -0
  16. package/dist/checks/auto-ban.check.js +43 -0
  17. package/dist/checks/blacklist.check.d.ts +4 -0
  18. package/dist/checks/blacklist.check.js +20 -0
  19. package/dist/checks/burst.check.d.ts +5 -0
  20. package/dist/checks/burst.check.js +28 -0
  21. package/dist/checks/payload.check.d.ts +5 -0
  22. package/dist/checks/payload.check.js +46 -0
  23. package/dist/checks/rate-limit.check.d.ts +9 -0
  24. package/dist/checks/rate-limit.check.js +59 -0
  25. package/dist/checks/slow-down.check.d.ts +5 -0
  26. package/dist/checks/slow-down.check.js +19 -0
  27. package/dist/checks/user-agent.check.d.ts +4 -0
  28. package/dist/checks/user-agent.check.js +31 -0
  29. package/dist/checks/whitelist.check.d.ts +4 -0
  30. package/dist/checks/whitelist.check.js +15 -0
  31. package/dist/decorators/blacklist.decorator.d.ts +2 -0
  32. package/dist/decorators/blacklist.decorator.js +7 -0
  33. package/dist/decorators/burst-limit.decorator.d.ts +2 -0
  34. package/dist/decorators/burst-limit.decorator.js +7 -0
  35. package/dist/decorators/max-payload.decorator.d.ts +2 -0
  36. package/dist/decorators/max-payload.decorator.js +7 -0
  37. package/dist/decorators/rate-limit.decorator.d.ts +2 -0
  38. package/dist/decorators/rate-limit.decorator.js +7 -0
  39. package/dist/decorators/skip-shield.decorator.d.ts +2 -0
  40. package/dist/decorators/skip-shield.decorator.js +7 -0
  41. package/dist/decorators/slow-down.decorator.d.ts +2 -0
  42. package/dist/decorators/slow-down.decorator.js +7 -0
  43. package/dist/decorators/user-agent-policy.decorator.d.ts +2 -0
  44. package/dist/decorators/user-agent-policy.decorator.js +7 -0
  45. package/dist/decorators/whitelist.decorator.d.ts +2 -0
  46. package/dist/decorators/whitelist.decorator.js +7 -0
  47. package/dist/exceptions/shield.exceptions.d.ts +23 -0
  48. package/dist/exceptions/shield.exceptions.js +40 -0
  49. package/dist/index.d.ts +32 -0
  50. package/dist/index.js +58 -0
  51. package/dist/shield.constants.d.ts +22 -0
  52. package/dist/shield.constants.js +33 -0
  53. package/dist/shield.engine.d.ts +21 -0
  54. package/dist/shield.engine.js +215 -0
  55. package/dist/shield.guard.d.ts +10 -0
  56. package/dist/shield.guard.js +89 -0
  57. package/dist/shield.middleware.d.ts +5 -0
  58. package/dist/shield.middleware.js +47 -0
  59. package/dist/shield.module.d.ts +14 -0
  60. package/dist/shield.module.js +105 -0
  61. package/dist/shield.types.d.ts +141 -0
  62. package/dist/shield.types.js +2 -0
  63. package/dist/storage/memory.storage.d.ts +30 -0
  64. package/dist/storage/memory.storage.js +201 -0
  65. package/dist/storage/redis.storage.d.ts +44 -0
  66. package/dist/storage/redis.storage.js +258 -0
  67. package/dist/storage/shield-storage.interface.d.ts +30 -0
  68. package/dist/storage/shield-storage.interface.js +2 -0
  69. package/dist/utils/headers.util.d.ts +5 -0
  70. package/dist/utils/headers.util.js +23 -0
  71. package/dist/utils/ip.util.d.ts +6 -0
  72. package/dist/utils/ip.util.js +100 -0
  73. package/dist/utils/key.util.d.ts +5 -0
  74. package/dist/utils/key.util.js +24 -0
  75. package/dist/utils/ua.util.d.ts +4 -0
  76. package/dist/utils/ua.util.js +26 -0
  77. package/package.json +77 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,361 @@
1
+ # nestjs-shield
2
+
3
+ Layered API protection for NestJS in a single drop-in module.
4
+
5
+ - **Rate limiting** with five algorithms — token bucket (default), sliding window counter, sliding window log, fixed window, leaky bucket
6
+ - **IP allow / block lists** with CIDR support
7
+ - **Auto-ban** that escalates repeated abusers to a temporary block
8
+ - **Slow-down** that adds progressive delay before issuing a hard 429
9
+ - **User-Agent filtering** with block / allow / require-present
10
+ - **Burst protection** — concurrent in-flight cap per IP (slowloris cover)
11
+ - **Payload limits** — body + header byte caps
12
+ - **Pluggable storage** — in-memory (default) or Redis (atomic Lua scripts) — bring your own adapter for anything else
13
+ - **Per-route overrides** via decorators
14
+ - **Standard rate-limit headers** (draft-7 by default, draft-6 supported)
15
+
16
+ No geo-fencing in v1. No dependency on `@nestjs/throttler` — the engine is standalone.
17
+
18
+ ## Install
19
+
20
+ From the public npm registry:
21
+
22
+ ```bash
23
+ npm install nestjs-shield
24
+ # optional, only if you use Redis storage
25
+ npm install ioredis
26
+ ```
27
+
28
+ Or from GitHub Packages (published as `@yassinos-coder/nestjs-shield`) — first create a project-local `.npmrc` mapping the scope to GitHub Packages and a personal access token with `read:packages`:
29
+
30
+ ```ini
31
+ # .npmrc
32
+ @yassinos-coder:registry=https://npm.pkg.github.com
33
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
34
+ ```
35
+
36
+ ```bash
37
+ GITHUB_TOKEN=ghp_xxx npm install @yassinos-coder/nestjs-shield
38
+ ```
39
+
40
+ Node ≥ 18, NestJS 9 / 10 / 11.
41
+
42
+ ## Quick start
43
+
44
+ ### Option A — global guard via `forRoot` (recommended)
45
+
46
+ ```ts
47
+ // app.module.ts
48
+ import { Module } from '@nestjs/common';
49
+ import { ShieldModule } from 'nestjs-shield';
50
+
51
+ @Module({
52
+ imports: [
53
+ ShieldModule.forRoot({
54
+ rateLimit: { algorithm: 'token-bucket', limit: 60, ttl: 60_000 },
55
+ autoBan: { threshold: 10, window: 60_000, banDuration: 5 * 60_000, escalate: true },
56
+ blacklist: { ips: ['10.10.10.10'] },
57
+ }),
58
+ ],
59
+ })
60
+ export class AppModule {}
61
+ ```
62
+
63
+ ### Option B — middleware-style, configured from `main.ts`
64
+
65
+ ```ts
66
+ // main.ts
67
+ import { NestFactory } from '@nestjs/core';
68
+ import { Shield } from 'nestjs-shield';
69
+ import { AppModule } from './app.module';
70
+
71
+ async function bootstrap() {
72
+ const app = await NestFactory.create(AppModule);
73
+ Shield.applyTo(app, {
74
+ rateLimit: { limit: 60, ttl: 60_000 },
75
+ burst: { maxConcurrent: 50 },
76
+ });
77
+ await app.listen(3000);
78
+ }
79
+ bootstrap();
80
+ ```
81
+
82
+ You can use both together — when you do, `applyTo` resolves the engine already created by `ShieldModule` (one config drives both).
83
+
84
+ ### `forRootAsync` with ConfigService
85
+
86
+ ```ts
87
+ ShieldModule.forRootAsync({
88
+ imports: [ConfigModule],
89
+ inject: [ConfigService],
90
+ useFactory: (cfg: ConfigService) => ({
91
+ rateLimit: {
92
+ limit: cfg.get<number>('RATE_LIMIT', 60),
93
+ ttl: cfg.get<number>('RATE_TTL', 60_000),
94
+ },
95
+ storage: { type: 'redis', client: new Redis(cfg.get('REDIS_URL')!) },
96
+ }),
97
+ });
98
+ ```
99
+
100
+ ## Configuration reference
101
+
102
+ ```ts
103
+ interface ShieldConfig {
104
+ enabled?: boolean; // default true
105
+ trustProxy?: boolean | number; // false; number = trust N proxies left of the client
106
+ ipResolver?: (req) => string; // your own IP extraction
107
+
108
+ storage?:
109
+ | 'memory'
110
+ | { type: 'memory'; maxKeys?: number } // default 100_000
111
+ | { type: 'redis'; client: Redis; keyPrefix?: string }
112
+ | ShieldStorage; // bring your own
113
+
114
+ rateLimit?: {
115
+ algorithm?: 'token-bucket' | 'sliding-window' | 'sliding-window-log' | 'fixed-window' | 'leaky-bucket';
116
+ limit: number;
117
+ ttl: number; // ms
118
+ keyBy?: 'ip' | { header: string } | ((req) => string);
119
+ skip?: (req) => boolean;
120
+ headers?: boolean; // default true
121
+ standardHeaders?: 'draft-6' | 'draft-7';
122
+ };
123
+
124
+ whitelist?: { ips?: string[]; cidrs?: string[] };
125
+ blacklist?: { ips?: string[]; cidrs?: string[]; statusCode?: number };
126
+
127
+ autoBan?: {
128
+ threshold: number; // # of 429/403s before ban
129
+ window: number; // ms — count window
130
+ banDuration: number; // ms — initial ban
131
+ escalate?: boolean; // 2× each repeat ban, default true
132
+ };
133
+
134
+ slowDown?: {
135
+ delayAfter: number;
136
+ delayMs: number | ((hit: number) => number);
137
+ maxDelayMs?: number;
138
+ };
139
+
140
+ userAgent?: {
141
+ block?: (string | RegExp)[];
142
+ allow?: (string | RegExp)[]; // overrides block when matched
143
+ requirePresent?: boolean;
144
+ };
145
+
146
+ burst?: { maxConcurrent: number };
147
+
148
+ payload?: { maxBodyBytes?: number; maxHeaderBytes?: number };
149
+
150
+ response?: {
151
+ rateLimit429?: { message?: string; code?: string; includeRetryAfter?: boolean };
152
+ blocked403?: { message?: string; code?: string };
153
+ payload413?: { message?: string; code?: string };
154
+ onReject?: (req, res, info) => void; // observability
155
+ };
156
+ }
157
+ ```
158
+
159
+ ## Algorithms
160
+
161
+ All algorithms share the same `{ limit, ttl }` knobs and the same storage interface, so switching is one config line.
162
+
163
+ | Algorithm | Storage cost | Burst behavior | Smoothness | Use when |
164
+ | --- | --- | --- | --- | --- |
165
+ | `token-bucket` (default) | 2 small numbers per IP | Allows initial burst up to `limit`, then drips | Smooth | General-purpose bursty APIs |
166
+ | `sliding-window` | 2 counters | Cannot 2× at window edges | Smooth | When fairness matters and storage is precious |
167
+ | `sliding-window-log` | one sorted set | Exact | Exact | Strict compliance, smaller scale |
168
+ | `fixed-window` | 1 counter | Allows up to 2× at window boundary | Stepped | Cheapest, very large scale, edge-burst OK |
169
+ | `leaky-bucket` | 2 small numbers per IP | Queue overflow rejects | Very smooth | Outbound shaping, traffic smoothing |
170
+
171
+ ## Pipeline order
172
+
173
+ Every request runs the chain in this fixed order, short-circuiting on the first reject:
174
+
175
+ 1. `whitelist` — allow ⇒ skip everything else
176
+ 2. `blacklist` — block ⇒ 403
177
+ 3. `auto-ban` — banned ⇒ 403 with `Retry-After`
178
+ 4. `user-agent` — blocked ⇒ 403
179
+ 5. `payload` — too large ⇒ 413
180
+ 6. `burst` — too many in-flight ⇒ 429
181
+ 7. `rate-limit` — over limit ⇒ 429, also feeds auto-ban
182
+ 8. `slow-down` — in soft zone ⇒ delay, then allow
183
+
184
+ ## Decorators (per-route overrides)
185
+
186
+ ```ts
187
+ import {
188
+ RateLimit, SkipShield, Blacklist, Whitelist,
189
+ SlowDown, MaxPayload, BurstLimit, UserAgentPolicy,
190
+ } from 'nestjs-shield';
191
+
192
+ @Controller('api')
193
+ export class ApiController {
194
+ @Get('hot')
195
+ @RateLimit({ algorithm: 'sliding-window-log', limit: 5, ttl: 10_000 })
196
+ hot() {}
197
+
198
+ @Get('health')
199
+ @SkipShield() // bypass all checks
200
+ health() {}
201
+
202
+ @Get('mixed')
203
+ @SkipShield('rate-limit', 'slow-down') // skip specific layers only
204
+ mixed() {}
205
+
206
+ @Post('upload')
207
+ @MaxPayload({ maxBodyBytes: 1_000_000 })
208
+ upload() {}
209
+
210
+ @Get('strict-ua')
211
+ @UserAgentPolicy({ block: [/curl/i], requirePresent: true })
212
+ strictUa() {}
213
+
214
+ @Get('vip')
215
+ @Whitelist({ cidrs: ['192.168.0.0/16'] })
216
+ vip() {}
217
+
218
+ @Get('blocked-net')
219
+ @Blacklist({ cidrs: ['203.0.113.0/24'] })
220
+ blocked() {}
221
+
222
+ @Get('long')
223
+ @BurstLimit({ maxConcurrent: 2 })
224
+ long() {}
225
+
226
+ @Get('slow')
227
+ @SlowDown({ delayAfter: 5, delayMs: 250 })
228
+ slow() {}
229
+ }
230
+ ```
231
+
232
+ Decorators merge over the global config — anything you omit falls through to the global setting.
233
+
234
+ ## Storage adapters
235
+
236
+ ### Memory (default)
237
+
238
+ ```ts
239
+ ShieldModule.forRoot({
240
+ storage: { type: 'memory', maxKeys: 200_000 }, // LRU evicts oldest 10% when full
241
+ });
242
+ ```
243
+
244
+ Per-process. Counters reset on restart. Fine for single-instance services.
245
+
246
+ ### Redis (multi-pod)
247
+
248
+ ```ts
249
+ import Redis from 'ioredis';
250
+
251
+ ShieldModule.forRoot({
252
+ storage: {
253
+ type: 'redis',
254
+ client: new Redis(process.env.REDIS_URL!),
255
+ keyPrefix: 'myapp', // optional
256
+ },
257
+ });
258
+ ```
259
+
260
+ All algorithms use atomic Lua scripts loaded via `defineCommand`, so token-bucket math is race-free across pods on the same key.
261
+
262
+ ### Bring your own
263
+
264
+ Implement `ShieldStorage` (15 methods, all small) and pass it as `storage: yourAdapter`.
265
+
266
+ ## Standard headers
267
+
268
+ Default is RFC draft-7:
269
+
270
+ ```http
271
+ RateLimit-Limit: 60
272
+ RateLimit-Remaining: 41
273
+ RateLimit-Reset: 23
274
+ RateLimit-Policy: 60;w=23
275
+ ```
276
+
277
+ On 429, also `Retry-After: <seconds>`. Switch to draft-6 (`X-RateLimit-*`) with `rateLimit.standardHeaders = 'draft-6'`.
278
+
279
+ ## Auto-ban
280
+
281
+ Every 429/403 emitted by the rate-limit, blacklist, or UA layer increments a violation counter for the offending IP, scoped by `autoBan.window`. When the counter crosses `autoBan.threshold`, the IP is placed in a temporary ban for `autoBan.banDuration`. If `escalate: true` (default), each subsequent ban doubles the previous duration.
282
+
283
+ Subsequent requests from a banned IP are rejected via a cheap `GET` on a `shield:ban:<ip>` key before any other check runs.
284
+
285
+ ## Slow-down
286
+
287
+ After `slowDown.delayAfter` requests in the rate-limit window, additional requests get an artificial delay computed from `delayMs` (number or function of the over-count), capped by `maxDelayMs`. The hard rate-limit still applies — slow-down only smooths approach to the cliff.
288
+
289
+ ## Behind a proxy
290
+
291
+ ```ts
292
+ ShieldModule.forRoot({
293
+ trustProxy: 1, // trust 1 hop — picks XFF[length - 2]
294
+ });
295
+ ```
296
+
297
+ Or provide a custom `ipResolver`. For Cloudflare, pulling `cf-connecting-ip` is a one-liner.
298
+
299
+ ## Observability
300
+
301
+ ```ts
302
+ response: {
303
+ onReject: (req, res, info) => {
304
+ metrics.increment('shield.reject', { layer: info.layer, status: info.status });
305
+ },
306
+ },
307
+ ```
308
+
309
+ `info` is `{ layer, ip, reason, status, retryAfterMs? }`. The hook is wrapped in try/catch and never breaks the pipeline.
310
+
311
+ ## Example
312
+
313
+ A runnable end-to-end example lives in [`example/`](./example) — see its README.
314
+
315
+ ## Publishing
316
+
317
+ This repo publishes to two registries with one workflow ([.github/workflows/publish.yml](.github/workflows/publish.yml)):
318
+
319
+ | Registry | Name there | Auth |
320
+ | --- | --- | --- |
321
+ | npm public (`registry.npmjs.org`) | `nestjs-shield` | `NPM_TOKEN` repo secret |
322
+ | GitHub Packages (`npm.pkg.github.com`) | `@yassinos-coder/nestjs-shield` | auto `GITHUB_TOKEN` |
323
+
324
+ The workflow runs on each published GitHub Release, on pushes of `v*.*.*` tags, and via manual dispatch (where you can pick `npm`, `github`, or `both`). The GitHub Packages job renames the package on the fly with `npm pkg set name=@yassinos-coder/nestjs-shield` so the same source tree maps cleanly to both registries.
325
+
326
+ ### One-time setup
327
+
328
+ 1. **`NPM_TOKEN` repo secret** — required only for the npm public job.
329
+ - Go to [npmjs.com → Access Tokens](https://www.npmjs.com/settings/yassinoscoder/tokens) → *Generate New Token* → *Classic Token* → *Automation* (skips 2FA).
330
+ - In the GitHub repo: *Settings → Secrets and variables → Actions → New repository secret* → name `NPM_TOKEN`, paste the token.
331
+ 2. **`GITHUB_TOKEN`** — nothing to do. GitHub Actions provides this automatically.
332
+
333
+ ### Releasing
334
+
335
+ ```bash
336
+ # bump version + tag
337
+ npm version 1.0.1
338
+ git push --follow-tags
339
+ ```
340
+
341
+ Or create a GitHub Release from the UI — the workflow fires on `release: published` too.
342
+
343
+ ### Manual local publish (alternative)
344
+
345
+ If you'd rather publish from your machine instead of CI:
346
+
347
+ ```bash
348
+ # npm public
349
+ npm login # browser flow
350
+ npm publish --access public
351
+
352
+ # GitHub Packages — needs a PAT with write:packages
353
+ echo "//npm.pkg.github.com/:_authToken=ghp_xxx" >> ~/.npmrc
354
+ npm pkg set name=@yassinos-coder/nestjs-shield
355
+ npm publish --registry=https://npm.pkg.github.com --access public
356
+ npm pkg set name=nestjs-shield # revert
357
+ ```
358
+
359
+ ## License
360
+
361
+ MIT
@@ -0,0 +1,4 @@
1
+ import type { ShieldStorage, WindowResult } from '../storage/shield-storage.interface';
2
+ export declare class FixedWindow {
3
+ static check(storage: ShieldStorage, key: string, limit: number, ttlMs: number): Promise<WindowResult>;
4
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FixedWindow = void 0;
4
+ class FixedWindow {
5
+ static async check(storage, key, limit, ttlMs) {
6
+ return storage.fixedWindow(key, ttlMs, limit);
7
+ }
8
+ }
9
+ exports.FixedWindow = FixedWindow;
@@ -0,0 +1,4 @@
1
+ import type { ShieldStorage, TokenBucketResult } from '../storage/shield-storage.interface';
2
+ export declare class LeakyBucket {
3
+ static check(storage: ShieldStorage, key: string, limit: number, ttlMs: number): Promise<TokenBucketResult>;
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LeakyBucket = void 0;
4
+ class LeakyBucket {
5
+ static async check(storage, key, limit, ttlMs) {
6
+ const leakPerMs = limit / ttlMs;
7
+ return storage.leakyBucket(key, limit, leakPerMs);
8
+ }
9
+ }
10
+ exports.LeakyBucket = LeakyBucket;
@@ -0,0 +1,4 @@
1
+ import type { ShieldStorage, WindowResult } from '../storage/shield-storage.interface';
2
+ export declare class SlidingWindowCounter {
3
+ static check(storage: ShieldStorage, key: string, limit: number, ttlMs: number): Promise<WindowResult>;
4
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SlidingWindowCounter = void 0;
4
+ class SlidingWindowCounter {
5
+ static async check(storage, key, limit, ttlMs) {
6
+ return storage.slidingWindowCounter(key, ttlMs, limit);
7
+ }
8
+ }
9
+ exports.SlidingWindowCounter = SlidingWindowCounter;
@@ -0,0 +1,4 @@
1
+ import type { ShieldStorage, WindowResult } from '../storage/shield-storage.interface';
2
+ export declare class SlidingWindowLog {
3
+ static check(storage: ShieldStorage, key: string, limit: number, ttlMs: number): Promise<WindowResult>;
4
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SlidingWindowLog = void 0;
4
+ class SlidingWindowLog {
5
+ static async check(storage, key, limit, ttlMs) {
6
+ return storage.slidingWindowLog(key, ttlMs, limit);
7
+ }
8
+ }
9
+ exports.SlidingWindowLog = SlidingWindowLog;
@@ -0,0 +1,4 @@
1
+ import type { ShieldStorage, TokenBucketResult } from '../storage/shield-storage.interface';
2
+ export declare class TokenBucket {
3
+ static check(storage: ShieldStorage, key: string, limit: number, ttlMs: number): Promise<TokenBucketResult>;
4
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TokenBucket = void 0;
4
+ class TokenBucket {
5
+ static async check(storage, key, limit, ttlMs) {
6
+ const refillPerMs = limit / ttlMs;
7
+ return storage.consumeToken(key, limit, refillPerMs, 1);
8
+ }
9
+ }
10
+ exports.TokenBucket = TokenBucket;
@@ -0,0 +1,8 @@
1
+ import type { ShieldConfig } from './shield.types';
2
+ export interface NestAppLike {
3
+ use: (...handlers: unknown[]) => unknown;
4
+ get?: <T = unknown>(token: unknown) => T;
5
+ }
6
+ export declare class Shield {
7
+ static applyTo(app: NestAppLike, config?: ShieldConfig): void;
8
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Shield = void 0;
4
+ const shield_engine_1 = require("./shield.engine");
5
+ const shield_middleware_1 = require("./shield.middleware");
6
+ const shield_constants_1 = require("./shield.constants");
7
+ const memory_storage_1 = require("./storage/memory.storage");
8
+ const redis_storage_1 = require("./storage/redis.storage");
9
+ function buildStorage(option) {
10
+ if (!option || option === 'memory')
11
+ return new memory_storage_1.MemoryStorage();
12
+ if (typeof option === 'object' && 'type' in option) {
13
+ if (option.type === 'memory')
14
+ return new memory_storage_1.MemoryStorage({ maxKeys: option.maxKeys });
15
+ if (option.type === 'redis') {
16
+ return new redis_storage_1.RedisStorage({
17
+ client: option.client,
18
+ keyPrefix: option.keyPrefix,
19
+ });
20
+ }
21
+ }
22
+ return option;
23
+ }
24
+ class Shield {
25
+ static applyTo(app, config = {}) {
26
+ let engine = null;
27
+ if (typeof app.get === 'function') {
28
+ try {
29
+ engine = app.get(shield_constants_1.SHIELD_ENGINE);
30
+ }
31
+ catch {
32
+ engine = null;
33
+ }
34
+ }
35
+ if (!engine) {
36
+ const storage = buildStorage(config.storage);
37
+ engine = new shield_engine_1.ShieldEngine(config, storage);
38
+ }
39
+ app.use((0, shield_middleware_1.createShieldMiddleware)(engine));
40
+ }
41
+ }
42
+ exports.Shield = Shield;
@@ -0,0 +1,6 @@
1
+ import type { ShieldStorage } from '../storage/shield-storage.interface';
2
+ import type { AutoBanConfig, CheckOutcome } from '../shield.types';
3
+ export declare class AutoBanCheck {
4
+ static check(storage: ShieldStorage, ip: string, config?: AutoBanConfig): Promise<CheckOutcome>;
5
+ static recordViolation(storage: ShieldStorage, ip: string, config?: AutoBanConfig): Promise<void>;
6
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AutoBanCheck = void 0;
4
+ const shield_constants_1 = require("../shield.constants");
5
+ class AutoBanCheck {
6
+ static async check(storage, ip, config) {
7
+ if (!config)
8
+ return { allowed: true };
9
+ const banKey = `${shield_constants_1.KEY_BAN}:${ip}`;
10
+ const raw = await storage.get(banKey);
11
+ if (!raw)
12
+ return { allowed: true };
13
+ const expiresAt = Number(raw);
14
+ const remaining = Math.max(0, expiresAt - Date.now());
15
+ if (remaining <= 0) {
16
+ await storage.delete(banKey);
17
+ return { allowed: true };
18
+ }
19
+ return {
20
+ allowed: false,
21
+ layer: 'auto-ban',
22
+ status: 403,
23
+ reason: 'IP temporarily banned for repeated violations',
24
+ retryAfterMs: remaining,
25
+ };
26
+ }
27
+ static async recordViolation(storage, ip, config) {
28
+ if (!config)
29
+ return;
30
+ const violationsKey = `${shield_constants_1.KEY_VIOLATIONS}:${ip}`;
31
+ const { count } = await storage.increment(violationsKey, config.window);
32
+ if (count < config.threshold)
33
+ return;
34
+ const banCountKey = `${shield_constants_1.KEY_BAN_COUNT}:${ip}`;
35
+ const banCountResult = await storage.increment(banCountKey, config.banDuration * 16);
36
+ const escalation = config.escalate === false ? 1 : Math.pow(2, banCountResult.count - 1);
37
+ const duration = config.banDuration * escalation;
38
+ const expiresAt = Date.now() + duration;
39
+ await storage.set(`${shield_constants_1.KEY_BAN}:${ip}`, String(expiresAt), duration);
40
+ await storage.delete(violationsKey);
41
+ }
42
+ }
43
+ exports.AutoBanCheck = AutoBanCheck;
@@ -0,0 +1,4 @@
1
+ import type { BlacklistConfig, CheckOutcome } from '../shield.types';
2
+ export declare class BlacklistCheck {
3
+ static run(ip: string, config?: BlacklistConfig): CheckOutcome;
4
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlacklistCheck = void 0;
4
+ const ip_util_1 = require("../utils/ip.util");
5
+ class BlacklistCheck {
6
+ static run(ip, config) {
7
+ if (!config)
8
+ return { allowed: true };
9
+ const hit = ip_util_1.IpUtil.matches(ip, config.ips, config.cidrs);
10
+ if (!hit)
11
+ return { allowed: true };
12
+ return {
13
+ allowed: false,
14
+ layer: 'blacklist',
15
+ status: config.statusCode ?? 403,
16
+ reason: 'IP is blacklisted',
17
+ };
18
+ }
19
+ }
20
+ exports.BlacklistCheck = BlacklistCheck;
@@ -0,0 +1,5 @@
1
+ import type { ShieldStorage } from '../storage/shield-storage.interface';
2
+ import type { BurstConfig, CheckOutcome } from '../shield.types';
3
+ export declare class BurstCheck {
4
+ static check(storage: ShieldStorage, ip: string, config?: BurstConfig): Promise<CheckOutcome>;
5
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BurstCheck = void 0;
4
+ const shield_constants_1 = require("../shield.constants");
5
+ class BurstCheck {
6
+ static async check(storage, ip, config) {
7
+ if (!config)
8
+ return { allowed: true };
9
+ const key = `${shield_constants_1.KEY_BURST}:${ip}`;
10
+ const current = await storage.incrementConcurrent(key);
11
+ if (current > config.maxConcurrent) {
12
+ await storage.decrementConcurrent(key);
13
+ return {
14
+ allowed: false,
15
+ layer: 'burst',
16
+ status: 429,
17
+ reason: `Concurrent request limit ${config.maxConcurrent} exceeded`,
18
+ };
19
+ }
20
+ return {
21
+ allowed: true,
22
+ release: async () => {
23
+ await storage.decrementConcurrent(key);
24
+ },
25
+ };
26
+ }
27
+ }
28
+ exports.BurstCheck = BurstCheck;
@@ -0,0 +1,5 @@
1
+ import type { AnyRequest, CheckOutcome, PayloadConfig } from '../shield.types';
2
+ export declare class PayloadCheck {
3
+ static run(req: AnyRequest, config?: PayloadConfig): CheckOutcome;
4
+ private static estimateHeaderBytes;
5
+ }