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.
- package/README.md +123 -6
- package/dist/block-manager.d.ts +134 -0
- package/dist/block-manager.d.ts.map +1 -0
- package/dist/block-manager.js +425 -0
- package/dist/block-manager.js.map +1 -0
- package/dist/cli.d.ts +141 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +784 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +148 -0
- package/dist/config.js.map +1 -0
- package/dist/detector.d.ts +56 -0
- package/dist/detector.d.ts.map +1 -0
- package/dist/detector.js +81 -0
- package/dist/detector.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +120 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +141 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/middleware.d.ts +68 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +272 -0
- package/dist/middleware.js.map +1 -0
- package/dist/next.d.ts +73 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +486 -0
- package/dist/next.js.map +1 -0
- package/dist/rate-limiter.d.ts +75 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +153 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/next.d.ts +1 -0
- package/next.js +2 -0
- package/package.json +34 -5
- package/src/block-manager.js +0 -326
- package/src/cli.js +0 -680
- package/src/config.js +0 -165
- package/src/detector.js +0 -88
- package/src/index.js +0 -97
- package/src/logger.js +0 -279
- package/src/middleware.js +0 -288
- package/src/rate-limiter.js +0 -186
package/src/block-manager.js
DELETED
|
@@ -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
|
-
};
|