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
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MemoryStorage = void 0;
4
+ const shield_constants_1 = require("../shield.constants");
5
+ class MemoryStorage {
6
+ constructor(opts = {}) {
7
+ this.map = new Map();
8
+ this.buckets = new Map();
9
+ this.logs = new Map();
10
+ this.counters = new Map();
11
+ this.sweepTimer = null;
12
+ this.maxKeys = opts.maxKeys ?? shield_constants_1.DEFAULT_MEMORY_MAX_KEYS;
13
+ this.startSweeper();
14
+ }
15
+ async increment(key, ttlMs, by = 1) {
16
+ const now = Date.now();
17
+ const existing = this.map.get(key);
18
+ if (!existing || existing.expiresAt <= now) {
19
+ const entry = { value: String(by), expiresAt: now + ttlMs };
20
+ this.set_(key, entry);
21
+ return { count: by, expiresAt: entry.expiresAt };
22
+ }
23
+ const next = Number(existing.value) + by;
24
+ existing.value = String(next);
25
+ return { count: next, expiresAt: existing.expiresAt };
26
+ }
27
+ async consumeToken(key, capacity, refillPerMs, cost = 1) {
28
+ const now = Date.now();
29
+ let bucket = this.buckets.get(key);
30
+ if (!bucket || bucket.expiresAt <= now) {
31
+ bucket = { tokens: capacity, last: now, expiresAt: now + this.ttlFromRefill(capacity, refillPerMs) };
32
+ this.evictIfNeeded(this.buckets);
33
+ this.buckets.set(key, bucket);
34
+ }
35
+ const elapsed = now - bucket.last;
36
+ bucket.tokens = Math.min(capacity, bucket.tokens + elapsed * refillPerMs);
37
+ bucket.last = now;
38
+ bucket.expiresAt = now + this.ttlFromRefill(capacity, refillPerMs);
39
+ const allowed = bucket.tokens >= cost;
40
+ if (allowed)
41
+ bucket.tokens -= cost;
42
+ const deficit = Math.max(0, cost - bucket.tokens);
43
+ const resetMs = refillPerMs > 0 ? Math.ceil(deficit / refillPerMs) : 0;
44
+ return {
45
+ allowed,
46
+ remaining: Math.max(0, Math.floor(bucket.tokens)),
47
+ resetMs,
48
+ limit: capacity,
49
+ };
50
+ }
51
+ async fixedWindow(key, ttlMs, limit) {
52
+ const now = Date.now();
53
+ const existing = this.map.get(key);
54
+ let count;
55
+ let expiresAt;
56
+ if (!existing || existing.expiresAt <= now) {
57
+ expiresAt = now + ttlMs;
58
+ count = 1;
59
+ this.set_(key, { value: '1', expiresAt });
60
+ }
61
+ else {
62
+ count = Number(existing.value) + 1;
63
+ existing.value = String(count);
64
+ expiresAt = existing.expiresAt;
65
+ }
66
+ return { count, allowed: count <= limit, resetMs: Math.max(0, expiresAt - now), limit };
67
+ }
68
+ async slidingWindowCounter(key, ttlMs, limit) {
69
+ const now = Date.now();
70
+ const windowStart = Math.floor(now / ttlMs) * ttlMs;
71
+ const currentKey = `${key}:c:${windowStart}`;
72
+ const prevKey = `${key}:c:${windowStart - ttlMs}`;
73
+ const elapsedInWindow = now - windowStart;
74
+ const weight = 1 - elapsedInWindow / ttlMs;
75
+ const current = await this.increment(currentKey, ttlMs * 2);
76
+ const prevRaw = this.map.get(prevKey);
77
+ const prevCount = prevRaw && prevRaw.expiresAt > now ? Number(prevRaw.value) : 0;
78
+ const weighted = prevCount * weight + current.count;
79
+ const allowed = weighted <= limit;
80
+ const resetMs = Math.max(0, ttlMs - elapsedInWindow);
81
+ return { count: Math.ceil(weighted), allowed, resetMs, limit };
82
+ }
83
+ async slidingWindowLog(key, ttlMs, limit, now = Date.now()) {
84
+ let log = this.logs.get(key);
85
+ if (!log) {
86
+ log = { hits: [], expiresAt: now + ttlMs };
87
+ this.evictIfNeeded(this.logs);
88
+ this.logs.set(key, log);
89
+ }
90
+ const threshold = now - ttlMs;
91
+ log.hits = log.hits.filter((t) => t > threshold);
92
+ const allowed = log.hits.length < limit;
93
+ if (allowed)
94
+ log.hits.push(now);
95
+ log.expiresAt = now + ttlMs;
96
+ const oldest = log.hits[0] ?? now;
97
+ const resetMs = Math.max(0, oldest + ttlMs - now);
98
+ return { count: log.hits.length, allowed, resetMs, limit };
99
+ }
100
+ async leakyBucket(key, capacity, leakPerMs) {
101
+ const now = Date.now();
102
+ let bucket = this.buckets.get(key);
103
+ if (!bucket || bucket.expiresAt <= now) {
104
+ bucket = { tokens: 0, last: now, expiresAt: now + this.ttlFromRefill(capacity, leakPerMs) };
105
+ this.evictIfNeeded(this.buckets);
106
+ this.buckets.set(key, bucket);
107
+ }
108
+ const elapsed = now - bucket.last;
109
+ bucket.tokens = Math.max(0, bucket.tokens - elapsed * leakPerMs);
110
+ bucket.last = now;
111
+ bucket.expiresAt = now + this.ttlFromRefill(capacity, leakPerMs);
112
+ const allowed = bucket.tokens + 1 <= capacity;
113
+ if (allowed)
114
+ bucket.tokens += 1;
115
+ const overflow = Math.max(0, bucket.tokens - capacity + 1);
116
+ const resetMs = leakPerMs > 0 ? Math.ceil(overflow / leakPerMs) : 0;
117
+ return {
118
+ allowed,
119
+ remaining: Math.max(0, capacity - Math.ceil(bucket.tokens)),
120
+ resetMs,
121
+ limit: capacity,
122
+ };
123
+ }
124
+ async get(key) {
125
+ const now = Date.now();
126
+ const e = this.map.get(key);
127
+ if (!e || e.expiresAt <= now)
128
+ return null;
129
+ return e.value;
130
+ }
131
+ async set(key, value, ttlMs) {
132
+ this.set_(key, { value, expiresAt: Date.now() + ttlMs });
133
+ }
134
+ async delete(key) {
135
+ this.map.delete(key);
136
+ this.buckets.delete(key);
137
+ this.logs.delete(key);
138
+ this.counters.delete(key);
139
+ }
140
+ async incrementConcurrent(key) {
141
+ const next = (this.counters.get(key) ?? 0) + 1;
142
+ this.counters.set(key, next);
143
+ return next;
144
+ }
145
+ async decrementConcurrent(key) {
146
+ const next = Math.max(0, (this.counters.get(key) ?? 0) - 1);
147
+ if (next === 0)
148
+ this.counters.delete(key);
149
+ else
150
+ this.counters.set(key, next);
151
+ return next;
152
+ }
153
+ dispose() {
154
+ if (this.sweepTimer) {
155
+ clearInterval(this.sweepTimer);
156
+ this.sweepTimer = null;
157
+ }
158
+ this.map.clear();
159
+ this.buckets.clear();
160
+ this.logs.clear();
161
+ this.counters.clear();
162
+ }
163
+ set_(key, entry) {
164
+ this.evictIfNeeded(this.map);
165
+ this.map.set(key, entry);
166
+ }
167
+ evictIfNeeded(target) {
168
+ if (target.size < this.maxKeys)
169
+ return;
170
+ const evictCount = Math.ceil(this.maxKeys * 0.1);
171
+ let i = 0;
172
+ for (const k of target.keys()) {
173
+ if (i++ >= evictCount)
174
+ break;
175
+ target.delete(k);
176
+ }
177
+ }
178
+ ttlFromRefill(capacity, ratePerMs) {
179
+ if (ratePerMs <= 0)
180
+ return 60_000;
181
+ return Math.ceil(capacity / ratePerMs) * 2;
182
+ }
183
+ startSweeper() {
184
+ this.sweepTimer = setInterval(() => this.sweep(), 30_000);
185
+ if (this.sweepTimer.unref)
186
+ this.sweepTimer.unref();
187
+ }
188
+ sweep() {
189
+ const now = Date.now();
190
+ for (const [k, v] of this.map)
191
+ if (v.expiresAt <= now)
192
+ this.map.delete(k);
193
+ for (const [k, v] of this.buckets)
194
+ if (v.expiresAt <= now)
195
+ this.buckets.delete(k);
196
+ for (const [k, v] of this.logs)
197
+ if (v.expiresAt <= now)
198
+ this.logs.delete(k);
199
+ }
200
+ }
201
+ exports.MemoryStorage = MemoryStorage;
@@ -0,0 +1,44 @@
1
+ import type { CounterResult, ShieldStorage, TokenBucketResult, WindowResult } from './shield-storage.interface';
2
+ type RedisLike = {
3
+ call: (...args: unknown[]) => Promise<unknown>;
4
+ eval: (...args: unknown[]) => Promise<unknown>;
5
+ incr: (key: string) => Promise<number>;
6
+ decr: (key: string) => Promise<number>;
7
+ incrby: (key: string, by: number) => Promise<number>;
8
+ pexpire: (key: string, ms: number) => Promise<number>;
9
+ pttl: (key: string) => Promise<number>;
10
+ get: (key: string) => Promise<string | null>;
11
+ set: (key: string, value: string, mode: string, ttl: number) => Promise<string | null>;
12
+ del: (key: string) => Promise<number>;
13
+ defineCommand?: (name: string, def: {
14
+ numberOfKeys: number;
15
+ lua: string;
16
+ }) => void;
17
+ [k: string]: unknown;
18
+ };
19
+ export interface RedisStorageOptions {
20
+ client: RedisLike;
21
+ keyPrefix?: string;
22
+ }
23
+ export declare class RedisStorage implements ShieldStorage {
24
+ private readonly client;
25
+ private readonly prefix;
26
+ private commandsDefined;
27
+ constructor(opts: RedisStorageOptions);
28
+ increment(key: string, ttlMs: number, by?: number): Promise<CounterResult>;
29
+ consumeToken(key: string, capacity: number, refillPerMs: number, cost?: number): Promise<TokenBucketResult>;
30
+ fixedWindow(key: string, ttlMs: number, limit: number): Promise<WindowResult>;
31
+ slidingWindowCounter(key: string, ttlMs: number, limit: number): Promise<WindowResult>;
32
+ slidingWindowLog(key: string, ttlMs: number, limit: number, now?: number): Promise<WindowResult>;
33
+ leakyBucket(key: string, capacity: number, leakPerMs: number): Promise<TokenBucketResult>;
34
+ get(key: string): Promise<string | null>;
35
+ set(key: string, value: string, ttlMs: number): Promise<void>;
36
+ delete(key: string): Promise<void>;
37
+ incrementConcurrent(key: string): Promise<number>;
38
+ decrementConcurrent(key: string): Promise<number>;
39
+ private k;
40
+ private ttlFor;
41
+ private tryDefineCommands;
42
+ private runScript;
43
+ }
44
+ export {};
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisStorage = void 0;
4
+ const TOKEN_BUCKET_LUA = `
5
+ local key = KEYS[1]
6
+ local capacity = tonumber(ARGV[1])
7
+ local refillPerMs = tonumber(ARGV[2])
8
+ local cost = tonumber(ARGV[3])
9
+ local now = tonumber(ARGV[4])
10
+ local ttlMs = tonumber(ARGV[5])
11
+
12
+ local data = redis.call('HMGET', key, 'tokens', 'last')
13
+ local tokens = tonumber(data[1])
14
+ local last = tonumber(data[2])
15
+
16
+ if tokens == nil then
17
+ tokens = capacity
18
+ last = now
19
+ end
20
+
21
+ local elapsed = math.max(0, now - last)
22
+ tokens = math.min(capacity, tokens + elapsed * refillPerMs)
23
+
24
+ local allowed = 0
25
+ if tokens >= cost then
26
+ tokens = tokens - cost
27
+ allowed = 1
28
+ end
29
+
30
+ redis.call('HMSET', key, 'tokens', tokens, 'last', now)
31
+ redis.call('PEXPIRE', key, ttlMs)
32
+
33
+ local deficit = math.max(0, cost - tokens)
34
+ local resetMs = 0
35
+ if refillPerMs > 0 then
36
+ resetMs = math.ceil(deficit / refillPerMs)
37
+ end
38
+
39
+ return { allowed, math.floor(tokens), resetMs }
40
+ `;
41
+ const LEAKY_BUCKET_LUA = `
42
+ local key = KEYS[1]
43
+ local capacity = tonumber(ARGV[1])
44
+ local leakPerMs = tonumber(ARGV[2])
45
+ local now = tonumber(ARGV[3])
46
+ local ttlMs = tonumber(ARGV[4])
47
+
48
+ local data = redis.call('HMGET', key, 'level', 'last')
49
+ local level = tonumber(data[1])
50
+ local last = tonumber(data[2])
51
+
52
+ if level == nil then
53
+ level = 0
54
+ last = now
55
+ end
56
+
57
+ local elapsed = math.max(0, now - last)
58
+ level = math.max(0, level - elapsed * leakPerMs)
59
+
60
+ local allowed = 0
61
+ if level + 1 <= capacity then
62
+ level = level + 1
63
+ allowed = 1
64
+ end
65
+
66
+ redis.call('HMSET', key, 'level', level, 'last', now)
67
+ redis.call('PEXPIRE', key, ttlMs)
68
+
69
+ local overflow = math.max(0, level - capacity + 1)
70
+ local resetMs = 0
71
+ if leakPerMs > 0 then
72
+ resetMs = math.ceil(overflow / leakPerMs)
73
+ end
74
+
75
+ return { allowed, math.max(0, capacity - math.ceil(level)), resetMs }
76
+ `;
77
+ const SLIDING_LOG_LUA = `
78
+ local key = KEYS[1]
79
+ local now = tonumber(ARGV[1])
80
+ local ttlMs = tonumber(ARGV[2])
81
+ local limit = tonumber(ARGV[3])
82
+ local threshold = now - ttlMs
83
+
84
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', threshold)
85
+ local count = redis.call('ZCARD', key)
86
+ local allowed = 0
87
+ if count < limit then
88
+ redis.call('ZADD', key, now, now .. ':' .. math.random())
89
+ count = count + 1
90
+ allowed = 1
91
+ end
92
+ redis.call('PEXPIRE', key, ttlMs)
93
+
94
+ local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
95
+ local resetMs = 0
96
+ if oldest[2] then
97
+ resetMs = math.max(0, tonumber(oldest[2]) + ttlMs - now)
98
+ end
99
+
100
+ return { allowed, count, resetMs }
101
+ `;
102
+ const SLIDING_COUNTER_LUA = `
103
+ local cur = KEYS[1]
104
+ local prev = KEYS[2]
105
+ local ttlMs = tonumber(ARGV[1])
106
+ local limit = tonumber(ARGV[2])
107
+ local weight = tonumber(ARGV[3])
108
+ local resetMs = tonumber(ARGV[4])
109
+
110
+ local prevCount = tonumber(redis.call('GET', prev) or '0')
111
+ local curCount = redis.call('INCR', cur)
112
+ redis.call('PEXPIRE', cur, ttlMs * 2)
113
+
114
+ local weighted = math.ceil(prevCount * weight + curCount)
115
+ local allowed = 0
116
+ if weighted <= limit then allowed = 1 end
117
+
118
+ return { allowed, weighted, resetMs }
119
+ `;
120
+ class RedisStorage {
121
+ constructor(opts) {
122
+ this.commandsDefined = false;
123
+ if (!opts || !opts.client) {
124
+ throw new Error('RedisStorage requires an ioredis client');
125
+ }
126
+ this.client = opts.client;
127
+ this.prefix = opts.keyPrefix ?? '';
128
+ this.tryDefineCommands();
129
+ }
130
+ async increment(key, ttlMs, by = 1) {
131
+ const k = this.k(key);
132
+ const count = by === 1 ? await this.client.incr(k) : await this.client.incrby(k, by);
133
+ await this.client.pexpire(k, ttlMs);
134
+ const ttl = await this.client.pttl(k);
135
+ return { count, expiresAt: Date.now() + Math.max(0, ttl) };
136
+ }
137
+ async consumeToken(key, capacity, refillPerMs, cost = 1) {
138
+ const k = this.k(key);
139
+ const ttlMs = this.ttlFor(capacity, refillPerMs);
140
+ const res = (await this.runScript('shieldTokenBucket', TOKEN_BUCKET_LUA, [k], [
141
+ capacity,
142
+ refillPerMs,
143
+ cost,
144
+ Date.now(),
145
+ ttlMs,
146
+ ]));
147
+ return {
148
+ allowed: res[0] === 1,
149
+ remaining: Math.max(0, Number(res[1])),
150
+ resetMs: Number(res[2]),
151
+ limit: capacity,
152
+ };
153
+ }
154
+ async fixedWindow(key, ttlMs, limit) {
155
+ const k = this.k(key);
156
+ const count = await this.client.incr(k);
157
+ if (count === 1)
158
+ await this.client.pexpire(k, ttlMs);
159
+ const ttl = await this.client.pttl(k);
160
+ return { count, allowed: count <= limit, resetMs: Math.max(0, ttl), limit };
161
+ }
162
+ async slidingWindowCounter(key, ttlMs, limit) {
163
+ const now = Date.now();
164
+ const windowStart = Math.floor(now / ttlMs) * ttlMs;
165
+ const cur = this.k(`${key}:c:${windowStart}`);
166
+ const prev = this.k(`${key}:c:${windowStart - ttlMs}`);
167
+ const elapsedInWindow = now - windowStart;
168
+ const weight = 1 - elapsedInWindow / ttlMs;
169
+ const resetMs = Math.max(0, ttlMs - elapsedInWindow);
170
+ const res = (await this.runScript('shieldSlidingCounter', SLIDING_COUNTER_LUA, [cur, prev], [ttlMs, limit, weight, resetMs]));
171
+ return {
172
+ allowed: res[0] === 1,
173
+ count: Number(res[1]),
174
+ resetMs: Number(res[2]),
175
+ limit,
176
+ };
177
+ }
178
+ async slidingWindowLog(key, ttlMs, limit, now = Date.now()) {
179
+ const k = this.k(key);
180
+ const res = (await this.runScript('shieldSlidingLog', SLIDING_LOG_LUA, [k], [now, ttlMs, limit]));
181
+ return {
182
+ allowed: res[0] === 1,
183
+ count: Number(res[1]),
184
+ resetMs: Number(res[2]),
185
+ limit,
186
+ };
187
+ }
188
+ async leakyBucket(key, capacity, leakPerMs) {
189
+ const k = this.k(key);
190
+ const ttlMs = this.ttlFor(capacity, leakPerMs);
191
+ const res = (await this.runScript('shieldLeakyBucket', LEAKY_BUCKET_LUA, [k], [
192
+ capacity,
193
+ leakPerMs,
194
+ Date.now(),
195
+ ttlMs,
196
+ ]));
197
+ return {
198
+ allowed: res[0] === 1,
199
+ remaining: Math.max(0, Number(res[1])),
200
+ resetMs: Number(res[2]),
201
+ limit: capacity,
202
+ };
203
+ }
204
+ async get(key) {
205
+ return this.client.get(this.k(key));
206
+ }
207
+ async set(key, value, ttlMs) {
208
+ await this.client.set(this.k(key), value, 'PX', ttlMs);
209
+ }
210
+ async delete(key) {
211
+ await this.client.del(this.k(key));
212
+ }
213
+ async incrementConcurrent(key) {
214
+ const k = this.k(`burst:${key}`);
215
+ const next = await this.client.incr(k);
216
+ await this.client.pexpire(k, 60_000);
217
+ return next;
218
+ }
219
+ async decrementConcurrent(key) {
220
+ const k = this.k(`burst:${key}`);
221
+ const next = await this.client.decr(k);
222
+ if (next <= 0)
223
+ await this.client.del(k);
224
+ return Math.max(0, next);
225
+ }
226
+ k(key) {
227
+ return this.prefix ? `${this.prefix}:${key}` : key;
228
+ }
229
+ ttlFor(capacity, ratePerMs) {
230
+ if (ratePerMs <= 0)
231
+ return 60_000;
232
+ return Math.ceil(capacity / ratePerMs) * 2;
233
+ }
234
+ tryDefineCommands() {
235
+ if (this.commandsDefined)
236
+ return;
237
+ if (typeof this.client.defineCommand !== 'function')
238
+ return;
239
+ try {
240
+ this.client.defineCommand('shieldTokenBucket', { numberOfKeys: 1, lua: TOKEN_BUCKET_LUA });
241
+ this.client.defineCommand('shieldLeakyBucket', { numberOfKeys: 1, lua: LEAKY_BUCKET_LUA });
242
+ this.client.defineCommand('shieldSlidingLog', { numberOfKeys: 1, lua: SLIDING_LOG_LUA });
243
+ this.client.defineCommand('shieldSlidingCounter', { numberOfKeys: 2, lua: SLIDING_COUNTER_LUA });
244
+ this.commandsDefined = true;
245
+ }
246
+ catch {
247
+ this.commandsDefined = false;
248
+ }
249
+ }
250
+ async runScript(name, lua, keys, args) {
251
+ const fn = this.client[name];
252
+ if (this.commandsDefined && typeof fn === 'function') {
253
+ return fn.call(this.client, ...keys, ...args);
254
+ }
255
+ return this.client.eval(lua, keys.length, ...keys, ...args);
256
+ }
257
+ }
258
+ exports.RedisStorage = RedisStorage;
@@ -0,0 +1,30 @@
1
+ export interface CounterResult {
2
+ count: number;
3
+ expiresAt: number;
4
+ }
5
+ export interface TokenBucketResult {
6
+ allowed: boolean;
7
+ remaining: number;
8
+ resetMs: number;
9
+ limit: number;
10
+ }
11
+ export interface WindowResult {
12
+ count: number;
13
+ allowed: boolean;
14
+ resetMs: number;
15
+ limit: number;
16
+ }
17
+ export interface ShieldStorage {
18
+ increment(key: string, ttlMs: number, by?: number): Promise<CounterResult>;
19
+ consumeToken(key: string, capacity: number, refillPerMs: number, cost?: number): Promise<TokenBucketResult>;
20
+ fixedWindow(key: string, ttlMs: number, limit: number): Promise<WindowResult>;
21
+ slidingWindowCounter(key: string, ttlMs: number, limit: number): Promise<WindowResult>;
22
+ slidingWindowLog(key: string, ttlMs: number, limit: number, now?: number): Promise<WindowResult>;
23
+ leakyBucket(key: string, capacity: number, leakPerMs: number): Promise<TokenBucketResult>;
24
+ get(key: string): Promise<string | null>;
25
+ set(key: string, value: string, ttlMs: number): Promise<void>;
26
+ delete(key: string): Promise<void>;
27
+ incrementConcurrent(key: string): Promise<number>;
28
+ decrementConcurrent(key: string): Promise<number>;
29
+ dispose?(): Promise<void> | void;
30
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { AnyResponse } from '../shield.types';
2
+ export declare class HeadersUtil {
3
+ static writeRateLimit(res: AnyResponse, standard: 'draft-6' | 'draft-7', limit: number, remaining: number, resetMs: number): void;
4
+ static writeRetryAfter(res: AnyResponse, retryAfterMs: number): void;
5
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HeadersUtil = void 0;
4
+ class HeadersUtil {
5
+ static writeRateLimit(res, standard, limit, remaining, resetMs) {
6
+ const resetSec = Math.max(0, Math.ceil(resetMs / 1000));
7
+ if (standard === 'draft-7') {
8
+ res.setHeader('RateLimit-Limit', String(limit));
9
+ res.setHeader('RateLimit-Remaining', String(Math.max(0, remaining)));
10
+ res.setHeader('RateLimit-Reset', String(resetSec));
11
+ res.setHeader('RateLimit-Policy', `${limit};w=${resetSec || 1}`);
12
+ }
13
+ else {
14
+ res.setHeader('X-RateLimit-Limit', String(limit));
15
+ res.setHeader('X-RateLimit-Remaining', String(Math.max(0, remaining)));
16
+ res.setHeader('X-RateLimit-Reset', String(Math.floor(Date.now() / 1000) + resetSec));
17
+ }
18
+ }
19
+ static writeRetryAfter(res, retryAfterMs) {
20
+ res.setHeader('Retry-After', String(Math.max(1, Math.ceil(retryAfterMs / 1000))));
21
+ }
22
+ }
23
+ exports.HeadersUtil = HeadersUtil;
@@ -0,0 +1,6 @@
1
+ import type { AnyRequest } from '../shield.types';
2
+ export declare class IpUtil {
3
+ static resolve(req: AnyRequest, trustProxy?: boolean | number): string;
4
+ static normalize(addr: string): string;
5
+ static matches(ip: string, ips?: string[], cidrs?: string[]): boolean;
6
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.IpUtil = void 0;
37
+ const ipaddr = __importStar(require("ipaddr.js"));
38
+ class IpUtil {
39
+ static resolve(req, trustProxy = false) {
40
+ const xff = req.headers['x-forwarded-for'];
41
+ if (trustProxy && xff) {
42
+ const list = Array.isArray(xff) ? xff.join(',') : String(xff);
43
+ const parts = list.split(',').map((s) => s.trim()).filter(Boolean);
44
+ if (parts.length) {
45
+ if (typeof trustProxy === 'number') {
46
+ const idx = Math.max(0, parts.length - trustProxy - 1);
47
+ if (parts[idx])
48
+ return IpUtil.normalize(parts[idx]);
49
+ }
50
+ return IpUtil.normalize(parts[0]);
51
+ }
52
+ }
53
+ if (trustProxy && Array.isArray(req.ips) && req.ips.length > 0) {
54
+ return IpUtil.normalize(req.ips[0]);
55
+ }
56
+ const direct = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || '0.0.0.0';
57
+ return IpUtil.normalize(direct);
58
+ }
59
+ static normalize(addr) {
60
+ const trimmed = addr.trim();
61
+ if (trimmed.startsWith('::ffff:'))
62
+ return trimmed.slice(7);
63
+ return trimmed;
64
+ }
65
+ static matches(ip, ips = [], cidrs = []) {
66
+ if (!ip)
67
+ return false;
68
+ if (ips.includes(ip))
69
+ return true;
70
+ if (cidrs.length === 0)
71
+ return false;
72
+ let parsed;
73
+ try {
74
+ parsed = ipaddr.parse(ip);
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ for (const cidr of cidrs) {
80
+ try {
81
+ const [range, prefix] = ipaddr.parseCIDR(cidr);
82
+ if (parsed.kind() !== range.kind())
83
+ continue;
84
+ if (parsed.kind() === 'ipv4') {
85
+ if (parsed.match([range, prefix]))
86
+ return true;
87
+ }
88
+ else {
89
+ if (parsed.match([range, prefix]))
90
+ return true;
91
+ }
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+ }
100
+ exports.IpUtil = IpUtil;
@@ -0,0 +1,5 @@
1
+ import type { AnyRequest, KeyByOption } from '../shield.types';
2
+ export declare class KeyUtil {
3
+ static fromRequest(req: AnyRequest, ip: string, keyBy?: KeyByOption): string;
4
+ static route(req: AnyRequest): string;
5
+ }