z-shield 0.1.0 → 0.1.2

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 (52) hide show
  1. package/README.md +123 -6
  2. package/dist/block-manager.d.ts +134 -0
  3. package/dist/block-manager.d.ts.map +1 -0
  4. package/dist/block-manager.js +425 -0
  5. package/dist/block-manager.js.map +1 -0
  6. package/dist/cli.d.ts +141 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +784 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/config.d.ts +15 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +148 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/detector.d.ts +56 -0
  15. package/dist/detector.d.ts.map +1 -0
  16. package/dist/detector.js +81 -0
  17. package/dist/detector.js.map +1 -0
  18. package/dist/index.d.ts +29 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +120 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/logger.d.ts +141 -0
  23. package/dist/logger.d.ts.map +1 -0
  24. package/dist/logger.js +347 -0
  25. package/dist/logger.js.map +1 -0
  26. package/dist/middleware.d.ts +68 -0
  27. package/dist/middleware.d.ts.map +1 -0
  28. package/dist/middleware.js +272 -0
  29. package/dist/middleware.js.map +1 -0
  30. package/dist/next.d.ts +73 -0
  31. package/dist/next.d.ts.map +1 -0
  32. package/dist/next.js +486 -0
  33. package/dist/next.js.map +1 -0
  34. package/dist/rate-limiter.d.ts +75 -0
  35. package/dist/rate-limiter.d.ts.map +1 -0
  36. package/dist/rate-limiter.js +153 -0
  37. package/dist/rate-limiter.js.map +1 -0
  38. package/dist/types.d.ts +199 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +6 -0
  41. package/dist/types.js.map +1 -0
  42. package/next.d.ts +1 -0
  43. package/next.js +2 -0
  44. package/package.json +34 -5
  45. package/src/block-manager.js +0 -326
  46. package/src/cli.js +0 -680
  47. package/src/config.js +0 -165
  48. package/src/detector.js +0 -88
  49. package/src/index.js +0 -97
  50. package/src/logger.js +0 -279
  51. package/src/middleware.js +0 -288
  52. package/src/rate-limiter.js +0 -186
@@ -1,326 +0,0 @@
1
- 'use strict';
2
-
3
- const net = require('node:net');
4
-
5
- /**
6
- * Normalizes an IP address for consistent comparison across IPv4 and IPv6.
7
- * - Trims whitespace
8
- * - Unmaps IPv4-mapped IPv6 addresses (e.g. '::ffff:192.168.1.1' -> '192.168.1.1')
9
- * - Normalizes IPv6 loopback ('::1' -> '127.0.0.1')
10
- * - Converts IPv6 hex characters to lowercase to prevent case-sensitive map misses
11
- * - Preserves invalid strings without converting them into fake valid IPs
12
- *
13
- * @param {*} ip
14
- * @returns {string} Normalized IP or 'unknown'
15
- */
16
- function normalizeIp(ip) {
17
- if (typeof ip !== 'string') {
18
- return 'unknown';
19
- }
20
- const trimmed = ip.trim();
21
- if (trimmed.length === 0) {
22
- return 'unknown';
23
- }
24
-
25
- const lower = trimmed.toLowerCase();
26
-
27
- // IPv6-mapped IPv4 addresses (e.g. '::ffff:192.168.1.1' or '::FFFF:192.168.1.1')
28
- if (lower.startsWith('::ffff:')) {
29
- const candidateIpv4 = trimmed.substring(7);
30
- if (net.isIP(candidateIpv4) === 4) {
31
- return candidateIpv4;
32
- }
33
- }
34
-
35
- // IPv6 loopback ('::1')
36
- if (lower === '::1') {
37
- return '127.0.0.1';
38
- }
39
-
40
- // Standard valid IPv4 or IPv6
41
- const ipType = net.isIP(trimmed);
42
- if (ipType === 6) {
43
- return lower;
44
- }
45
- if (ipType === 4) {
46
- return trimmed;
47
- }
48
-
49
- return trimmed;
50
- }
51
-
52
- /**
53
- * Validates whether an IP address is a valid IPv4 or IPv6 string.
54
- * Uses Node.js built-in net.isIP (returns 4 for IPv4, 6 for IPv6, 0 for invalid).
55
- *
56
- * @param {string} ip
57
- * @returns {boolean}
58
- */
59
- function isValidIp(ip) {
60
- if (typeof ip !== 'string' || ip.trim().length === 0) {
61
- return false;
62
- }
63
- const norm = normalizeIp(ip);
64
- return net.isIP(norm) !== 0;
65
- }
66
-
67
- /**
68
- * Parses block duration into milliseconds.
69
- * Supports numbers (milliseconds) or human-readable strings ('30s', '10m', '1h', '2d').
70
- *
71
- * @param {number|string} duration
72
- * @returns {number} duration in milliseconds
73
- */
74
- function parseDuration(duration) {
75
- if (typeof duration === 'number') {
76
- const ms = Math.floor(duration);
77
- if (!Number.isFinite(duration) || duration <= 0 || ms <= 0) {
78
- throw new TypeError('[Z-Shield] Block duration must be a positive number in milliseconds.');
79
- }
80
- return ms;
81
- }
82
-
83
- if (typeof duration === 'string') {
84
- const trimmed = duration.trim();
85
- const match = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
86
- if (!match) {
87
- throw new Error(`[Z-Shield] Invalid duration format: "${duration}". Use format like "10m", "1h", "30s", or milliseconds.`);
88
- }
89
- const val = parseFloat(match[1]);
90
- if (!Number.isFinite(val) || val <= 0) {
91
- throw new Error('[Z-Shield] Block duration must be greater than zero.');
92
- }
93
- const unit = (match[2] || 'ms').toLowerCase();
94
- let ms;
95
- switch (unit) {
96
- case 'ms': ms = Math.floor(val); break;
97
- case 's': ms = Math.floor(val * 1000); break;
98
- case 'm': ms = Math.floor(val * 60 * 1000); break;
99
- case 'h': ms = Math.floor(val * 60 * 60 * 1000); break;
100
- case 'd': ms = Math.floor(val * 24 * 60 * 60 * 1000); break;
101
- default: ms = Math.floor(val); break;
102
- }
103
- if (ms <= 0) {
104
- throw new Error('[Z-Shield] Block duration must be greater than zero.');
105
- }
106
- return ms;
107
- }
108
-
109
- throw new TypeError('[Z-Shield] Block duration must be a number (milliseconds) or a duration string like "10m".');
110
- }
111
-
112
- /**
113
- * BlockManager - In-Memory Temporary IP Block Management
114
- *
115
- * Characteristics:
116
- * - Process-Local: Blocks are held in RAM. They do NOT survive server restarts.
117
- * - Automatic Expiry: Block entries are checked against their expiration timestamp.
118
- * - Bounded Memory: Uses LRU eviction when `maxBlockedIps` is reached to prevent memory exhaustion.
119
- * - No Background Timers: Uses passive/lazy expiration on access to keep Node.js event loop idle.
120
- */
121
- class BlockManager {
122
- /**
123
- * @param {Object} [options={}]
124
- * @param {number} [options.blockDurationMs=900000] - Default block duration in ms (15 min)
125
- * @param {number} [options.maxBlockedIps=5000] - Max blocked IPs in memory
126
- */
127
- constructor(options = {}) {
128
- this.defaultDurationMs = options.blockDurationMs || 15 * 60 * 1000;
129
- this.maxBlockedIps = options.maxBlockedIps || 5000;
130
-
131
- /** @type {Map<string, { blockedAt: number, expiresAt: number, reason: string }>} */
132
- this.blocks = new Map();
133
- }
134
-
135
- /**
136
- * Temporarily blocks an IP address.
137
- *
138
- * @param {string} ip - Valid IPv4 or IPv6 address
139
- * @param {number|string} [duration=this.defaultDurationMs] - Duration in ms or string ('10m')
140
- * @param {string} [reason='Administrative block'] - Reason for block
141
- * @param {number} [now=Date.now()] - Current timestamp (injectable for testing)
142
- * @returns {{ ip: string, blockedAt: number, expiresAt: number, durationMs: number, reason: string }}
143
- */
144
- block(ip, duration = this.defaultDurationMs, reason = 'Administrative block', now = Date.now()) {
145
- if (!isValidIp(ip)) {
146
- throw new TypeError(`[Z-Shield] Cannot block invalid IP address: "${ip}".`);
147
- }
148
-
149
- const durationMs = parseDuration(duration);
150
- const cleanIp = normalizeIp(ip);
151
-
152
- // Bound memory: if at capacity, evict an old entry before inserting
153
- if (this.blocks.size >= this.maxBlockedIps && !this.blocks.has(cleanIp)) {
154
- this._evictOldest(now);
155
- }
156
-
157
- const entry = {
158
- blockedAt: now,
159
- expiresAt: now + durationMs,
160
- reason: String(reason || 'Administrative block')
161
- };
162
-
163
- // Refresh LRU order: delete if exists, then set
164
- this.blocks.delete(cleanIp);
165
- this.blocks.set(cleanIp, entry);
166
-
167
- return {
168
- ip: cleanIp,
169
- blockedAt: entry.blockedAt,
170
- expiresAt: entry.expiresAt,
171
- durationMs,
172
- reason: entry.reason
173
- };
174
- }
175
-
176
- /**
177
- * Manually unblocks an IP address.
178
- *
179
- * @param {string} ip - Client IP
180
- * @returns {boolean} True if IP was unblocked, false if it was not blocked
181
- */
182
- unblock(ip) {
183
- if (!isValidIp(ip)) {
184
- throw new TypeError(`[Z-Shield] Cannot unblock invalid IP address: "${ip}".`);
185
- }
186
- return this.blocks.delete(normalizeIp(ip));
187
- }
188
-
189
- /**
190
- * Checks whether an IP address is currently blocked.
191
- * Expired blocks are lazily pruned.
192
- *
193
- * @param {string} ip
194
- * @param {number} [now=Date.now()]
195
- * @returns {boolean}
196
- */
197
- isBlocked(ip, now = Date.now()) {
198
- if (!ip || typeof ip !== 'string') {
199
- return false;
200
- }
201
-
202
- const cleanIp = normalizeIp(ip);
203
- const entry = this.blocks.get(cleanIp);
204
-
205
- if (!entry) {
206
- return false;
207
- }
208
-
209
- // Lazy expiration check
210
- if (now >= entry.expiresAt) {
211
- this.blocks.delete(cleanIp);
212
- return false;
213
- }
214
-
215
- return true;
216
- }
217
-
218
- /**
219
- * Retrieves active block details for an IP, or null if not blocked/expired.
220
- *
221
- * @param {string} ip
222
- * @param {number} [now=Date.now()]
223
- * @returns {{ ip: string, blockedAt: number, expiresAt: number, remainingMs: number, reason: string } | null}
224
- */
225
- getBlockInfo(ip, now = Date.now()) {
226
- if (!this.isBlocked(ip, now)) {
227
- return null;
228
- }
229
-
230
- const cleanIp = normalizeIp(ip);
231
- const entry = this.blocks.get(cleanIp);
232
-
233
- return {
234
- ip: cleanIp,
235
- blockedAt: entry.blockedAt,
236
- expiresAt: entry.expiresAt,
237
- remainingMs: Math.max(0, entry.expiresAt - now),
238
- reason: entry.reason
239
- };
240
- }
241
-
242
- /**
243
- * Returns a list of all currently active blocked IPs.
244
- *
245
- * @param {number} [now=Date.now()]
246
- * @returns {Array<{ ip: string, blockedAt: number, expiresAt: number, remainingMs: number, reason: string }>}
247
- */
248
- listBlocks(now = Date.now()) {
249
- this.pruneExpired(now);
250
- const result = [];
251
- for (const [ip, entry] of this.blocks.entries()) {
252
- result.push({
253
- ip,
254
- blockedAt: entry.blockedAt,
255
- expiresAt: entry.expiresAt,
256
- remainingMs: Math.max(0, entry.expiresAt - now),
257
- reason: entry.reason
258
- });
259
- }
260
- return result;
261
- }
262
-
263
- /**
264
- * Scans and purges any expired blocks from memory.
265
- *
266
- * @param {number} [now=Date.now()]
267
- * @returns {number} Number of purged blocks
268
- */
269
- pruneExpired(now = Date.now()) {
270
- let pruned = 0;
271
- for (const [ip, entry] of this.blocks.entries()) {
272
- if (now >= entry.expiresAt) {
273
- this.blocks.delete(ip);
274
- pruned++;
275
- }
276
- }
277
- return pruned;
278
- }
279
-
280
- /**
281
- * Clears all blocks from memory.
282
- */
283
- clear() {
284
- this.blocks.clear();
285
- }
286
-
287
- /**
288
- * Number of IPs currently stored in memory.
289
- * @returns {number}
290
- */
291
- get size() {
292
- return this.blocks.size;
293
- }
294
-
295
- /**
296
- * Evicts the oldest entry to strictly uphold maxBlockedIps limit.
297
- * @private
298
- * @param {number} now
299
- */
300
- _evictOldest(now) {
301
- const iterator = this.blocks.entries();
302
- const first = iterator.next().value;
303
-
304
- if (!first) {
305
- return;
306
- }
307
-
308
- const [oldestIp, oldestEntry] = first;
309
-
310
- // If oldest is already expired, delete it
311
- if (now >= oldestEntry.expiresAt) {
312
- this.blocks.delete(oldestIp);
313
- return;
314
- }
315
-
316
- // Evict oldest LRU entry
317
- this.blocks.delete(oldestIp);
318
- }
319
- }
320
-
321
- module.exports = {
322
- BlockManager,
323
- isValidIp,
324
- normalizeIp,
325
- parseDuration
326
- };