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
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BlockManager = void 0;
7
+ exports.canonicalizeIpv6 = canonicalizeIpv6;
8
+ exports.normalizeIp = normalizeIp;
9
+ exports.isValidIp = isValidIp;
10
+ exports.parseDuration = parseDuration;
11
+ const node_net_1 = __importDefault(require("node:net"));
12
+ /**
13
+ * Canonicalizes an IPv6 address string according to RFC 5952.
14
+ * - Parses 16-bit hextets
15
+ * - Handles embedded IPv4 in IPv6 (including IPv4-mapped IPv6)
16
+ * - Normalizes loopback ('::1' and '0:0:0:0:0:0:0:1') to '127.0.0.1'
17
+ * - Normalizes IPv4-mapped addresses to standard IPv4
18
+ * - Compresses the longest run of 2 or more zeros with '::' (first run on tie)
19
+ * - Lowers all hexadecimal characters and removes leading zeros in each field
20
+ *
21
+ * @param ip - Valid IPv6 address string
22
+ * @returns Canonicalized representation
23
+ */
24
+ function canonicalizeIpv6(ip) {
25
+ let str = ip.toLowerCase().trim();
26
+ const zoneIndex = str.indexOf('%');
27
+ if (zoneIndex !== -1) {
28
+ str = str.substring(0, zoneIndex);
29
+ }
30
+ // Handle embedded IPv4 (e.g. ::ffff:192.0.2.1)
31
+ let embeddedIpv4 = null;
32
+ const lastColon = str.lastIndexOf(':');
33
+ if (lastColon !== -1) {
34
+ const candidateIpv4 = str.substring(lastColon + 1);
35
+ if (candidateIpv4.includes('.')) {
36
+ if (node_net_1.default.isIP(candidateIpv4) === 4) {
37
+ embeddedIpv4 = candidateIpv4.split('.').map(Number);
38
+ str = str.substring(0, lastColon) + ':0:0';
39
+ }
40
+ else {
41
+ return ip.toLowerCase().trim();
42
+ }
43
+ }
44
+ }
45
+ const parts = str.split('::');
46
+ if (parts.length > 2) {
47
+ return ip.toLowerCase().trim();
48
+ }
49
+ const head = parts[0] ? parts[0].split(':').filter(Boolean) : [];
50
+ const tail = parts.length === 2 && parts[1] ? parts[1].split(':').filter(Boolean) : [];
51
+ const totalHextets = head.length + tail.length;
52
+ if (parts.length === 1 && totalHextets !== 8) {
53
+ return ip.toLowerCase().trim();
54
+ }
55
+ if (parts.length === 2 && totalHextets >= 8) {
56
+ return ip.toLowerCase().trim();
57
+ }
58
+ const missingZeros = 8 - totalHextets;
59
+ const hextets = [];
60
+ for (const h of head) {
61
+ const val = parseInt(h, 16);
62
+ if (isNaN(val) || val < 0 || val > 0xffff) {
63
+ return ip.toLowerCase().trim();
64
+ }
65
+ hextets.push(val);
66
+ }
67
+ for (let i = 0; i < missingZeros; i++) {
68
+ hextets.push(0);
69
+ }
70
+ for (const t of tail) {
71
+ const val = parseInt(t, 16);
72
+ if (isNaN(val) || val < 0 || val > 0xffff) {
73
+ return ip.toLowerCase().trim();
74
+ }
75
+ hextets.push(val);
76
+ }
77
+ if (hextets.length !== 8) {
78
+ return ip.toLowerCase().trim();
79
+ }
80
+ if (embeddedIpv4 && embeddedIpv4.length === 4) {
81
+ hextets[6] = ((embeddedIpv4[0] ?? 0) << 8) | (embeddedIpv4[1] ?? 0);
82
+ hextets[7] = ((embeddedIpv4[2] ?? 0) << 8) | (embeddedIpv4[3] ?? 0);
83
+ }
84
+ // IPv4-mapped IPv6: ::ffff:w.x.y.z or 0:0:0:0:0:ffff:w.x.y.z
85
+ const isIpv4Mapped = hextets[0] === 0 &&
86
+ hextets[1] === 0 &&
87
+ hextets[2] === 0 &&
88
+ hextets[3] === 0 &&
89
+ hextets[4] === 0 &&
90
+ hextets[5] === 0xffff;
91
+ if (isIpv4Mapped) {
92
+ const h6 = hextets[6] ?? 0;
93
+ const h7 = hextets[7] ?? 0;
94
+ const b0 = (h6 >> 8) & 0xff;
95
+ const b1 = h6 & 0xff;
96
+ const b2 = (h7 >> 8) & 0xff;
97
+ const b3 = h7 & 0xff;
98
+ return `${b0}.${b1}.${b2}.${b3}`;
99
+ }
100
+ // IPv6 Loopback: ::1 or 0:0:0:0:0:0:0:1 -> 127.0.0.1
101
+ const isLoopback = hextets.slice(0, 7).every((h) => h === 0) && hextets[7] === 1;
102
+ if (isLoopback) {
103
+ return '127.0.0.1';
104
+ }
105
+ // Find longest contiguous run of zeros (length >= 2) under RFC 5952
106
+ let bestStart = -1;
107
+ let bestLen = 0;
108
+ let curStart = -1;
109
+ let curLen = 0;
110
+ for (let i = 0; i < 8; i++) {
111
+ if (hextets[i] === 0) {
112
+ if (curStart === -1) {
113
+ curStart = i;
114
+ curLen = 1;
115
+ }
116
+ else {
117
+ curLen++;
118
+ }
119
+ if (curLen > bestLen) {
120
+ bestStart = curStart;
121
+ bestLen = curLen;
122
+ }
123
+ }
124
+ else {
125
+ curStart = -1;
126
+ curLen = 0;
127
+ }
128
+ }
129
+ // If no zero sequence of 2 or more hextets exists, join all 8 hextets
130
+ if (bestLen < 2) {
131
+ return hextets.map((h) => h.toString(16)).join(':');
132
+ }
133
+ const headStr = hextets.slice(0, bestStart).map((h) => h.toString(16)).join(':');
134
+ const tailStr = hextets.slice(bestStart + bestLen).map((h) => h.toString(16)).join(':');
135
+ return `${headStr}::${tailStr}`;
136
+ }
137
+ /**
138
+ * Normalizes an IP address for consistent comparison across IPv4 and IPv6.
139
+ * - Trims whitespace
140
+ * - Canonicalizes IPv6 addresses according to RFC 5952 (zero compression, lowercase)
141
+ * - Unmaps all IPv4-mapped IPv6 variations (e.g. '::ffff:192.168.1.1' -> '192.168.1.1')
142
+ * - Normalizes all IPv6 loopback variants ('::1', '0:0:0:0:0:0:0:1') to '127.0.0.1'
143
+ * - Preserves invalid strings without converting them into fake valid IPs
144
+ *
145
+ * @param ip - Raw client IP candidate
146
+ * @returns Normalized IP or 'unknown'
147
+ */
148
+ function normalizeIp(ip) {
149
+ if (typeof ip !== 'string') {
150
+ return 'unknown';
151
+ }
152
+ const trimmed = ip.trim();
153
+ if (trimmed.length === 0) {
154
+ return 'unknown';
155
+ }
156
+ // Standard valid IPv4
157
+ const ipType = node_net_1.default.isIP(trimmed);
158
+ if (ipType === 4) {
159
+ return trimmed;
160
+ }
161
+ // Standard valid IPv6 (including loopback and IPv4-mapped)
162
+ if (ipType === 6) {
163
+ return canonicalizeIpv6(trimmed);
164
+ }
165
+ // Handle possible ::ffff:192.0.2.1 where net.isIP might return 0
166
+ const lower = trimmed.toLowerCase();
167
+ if (lower.startsWith('::ffff:')) {
168
+ const candidateIpv4 = trimmed.substring(7);
169
+ if (node_net_1.default.isIP(candidateIpv4) === 4) {
170
+ return candidateIpv4;
171
+ }
172
+ }
173
+ return trimmed;
174
+ }
175
+ /**
176
+ * Validates whether an IP address is a valid IPv4 or IPv6 string.
177
+ * Uses Node.js built-in net.isIP (returns 4 for IPv4, 6 for IPv6, 0 for invalid).
178
+ *
179
+ * @param ip - IP candidate string
180
+ * @returns True if valid IPv4 or IPv6
181
+ */
182
+ function isValidIp(ip) {
183
+ if (typeof ip !== 'string' || ip.trim().length === 0) {
184
+ return false;
185
+ }
186
+ const norm = normalizeIp(ip);
187
+ return node_net_1.default.isIP(norm) !== 0;
188
+ }
189
+ /**
190
+ * Parses block duration into milliseconds.
191
+ * Supports numbers (milliseconds) or human-readable strings ('30s', '10m', '1h', '2d').
192
+ *
193
+ * @param duration - Numeric ms or formatted string
194
+ * @returns Duration in milliseconds
195
+ */
196
+ function parseDuration(duration) {
197
+ if (typeof duration === 'number') {
198
+ const ms = Math.floor(duration);
199
+ if (!Number.isFinite(duration) || duration <= 0 || ms <= 0) {
200
+ throw new TypeError('[Z-Shield] Block duration must be a positive number in milliseconds.');
201
+ }
202
+ return ms;
203
+ }
204
+ if (typeof duration === 'string') {
205
+ const trimmed = duration.trim();
206
+ const match = trimmed.match(/^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
207
+ if (!match) {
208
+ throw new Error(`[Z-Shield] Invalid duration format: "${duration}". Use format like "10m", "1h", "30s", or milliseconds.`);
209
+ }
210
+ const rawVal = match[1];
211
+ if (rawVal === undefined) {
212
+ throw new Error('[Z-Shield] Block duration must be greater than zero.');
213
+ }
214
+ const val = parseFloat(rawVal);
215
+ if (!Number.isFinite(val) || val <= 0) {
216
+ throw new Error('[Z-Shield] Block duration must be greater than zero.');
217
+ }
218
+ const unit = (match[2] || 'ms').toLowerCase();
219
+ let ms;
220
+ switch (unit) {
221
+ case 'ms':
222
+ ms = Math.floor(val);
223
+ break;
224
+ case 's':
225
+ ms = Math.floor(val * 1000);
226
+ break;
227
+ case 'm':
228
+ ms = Math.floor(val * 60 * 1000);
229
+ break;
230
+ case 'h':
231
+ ms = Math.floor(val * 60 * 60 * 1000);
232
+ break;
233
+ case 'd':
234
+ ms = Math.floor(val * 24 * 60 * 60 * 1000);
235
+ break;
236
+ default:
237
+ ms = Math.floor(val);
238
+ break;
239
+ }
240
+ if (ms <= 0) {
241
+ throw new Error('[Z-Shield] Block duration must be greater than zero.');
242
+ }
243
+ return ms;
244
+ }
245
+ throw new TypeError('[Z-Shield] Block duration must be a number (milliseconds) or a duration string like "10m".');
246
+ }
247
+ /**
248
+ * BlockManager - In-Memory Temporary IP Block Management
249
+ *
250
+ * Characteristics:
251
+ * - Process-Local: Blocks are held in RAM. They do NOT survive server restarts.
252
+ * - Automatic Expiry: Block entries are checked against their expiration timestamp.
253
+ * - Bounded Memory: Uses LRU eviction when `maxBlockedIps` is reached to prevent memory exhaustion.
254
+ * - No Background Timers: Uses passive/lazy expiration on access to keep Node.js event loop idle.
255
+ */
256
+ class BlockManager {
257
+ defaultDurationMs;
258
+ maxBlockedIps;
259
+ blocks;
260
+ /**
261
+ * @param options - Block manager options or resolved config
262
+ */
263
+ constructor(options = {}) {
264
+ this.defaultDurationMs = options.blockDurationMs || 15 * 60 * 1000;
265
+ this.maxBlockedIps = options.maxBlockedIps || 5000;
266
+ this.blocks = new Map();
267
+ }
268
+ /**
269
+ * Temporarily blocks an IP address.
270
+ *
271
+ * @param ip - Valid IPv4 or IPv6 address
272
+ * @param duration - Duration in ms or string ('10m')
273
+ * @param reason - Reason for block
274
+ * @param now - Current timestamp (injectable for testing)
275
+ * @returns Created block record
276
+ */
277
+ block(ip, duration = this.defaultDurationMs, reason = 'Administrative block', now = Date.now()) {
278
+ if (!isValidIp(ip)) {
279
+ throw new TypeError(`[Z-Shield] Cannot block invalid IP address: "${ip}".`);
280
+ }
281
+ const durationMs = parseDuration(duration);
282
+ const cleanIp = normalizeIp(ip);
283
+ // Bound memory: if at capacity, evict an old entry before inserting
284
+ if (this.blocks.size >= this.maxBlockedIps && !this.blocks.has(cleanIp)) {
285
+ this._evictOldest(now);
286
+ }
287
+ const entry = {
288
+ blockedAt: now,
289
+ expiresAt: now + durationMs,
290
+ reason: String(reason || 'Administrative block')
291
+ };
292
+ // Refresh LRU order: delete if exists, then set
293
+ this.blocks.delete(cleanIp);
294
+ this.blocks.set(cleanIp, entry);
295
+ return {
296
+ ip: cleanIp,
297
+ blockedAt: entry.blockedAt,
298
+ expiresAt: entry.expiresAt,
299
+ durationMs,
300
+ reason: entry.reason
301
+ };
302
+ }
303
+ /**
304
+ * Manually unblocks an IP address.
305
+ *
306
+ * @param ip - Client IP
307
+ * @returns True if IP was unblocked, false if it was not blocked
308
+ */
309
+ unblock(ip) {
310
+ if (!isValidIp(ip)) {
311
+ throw new TypeError(`[Z-Shield] Cannot unblock invalid IP address: "${ip}".`);
312
+ }
313
+ return this.blocks.delete(normalizeIp(ip));
314
+ }
315
+ /**
316
+ * Checks whether an IP address is currently blocked.
317
+ * Expired blocks are lazily pruned.
318
+ *
319
+ * @param ip - Client IP
320
+ * @param now - Current timestamp
321
+ * @returns True if actively blocked
322
+ */
323
+ isBlocked(ip, now = Date.now()) {
324
+ if (!ip || typeof ip !== 'string') {
325
+ return false;
326
+ }
327
+ const cleanIp = normalizeIp(ip);
328
+ const entry = this.blocks.get(cleanIp);
329
+ if (!entry) {
330
+ return false;
331
+ }
332
+ // Lazy expiration check
333
+ if (now >= entry.expiresAt) {
334
+ this.blocks.delete(cleanIp);
335
+ return false;
336
+ }
337
+ return true;
338
+ }
339
+ /**
340
+ * Retrieves active block details for an IP, or null if not blocked/expired.
341
+ *
342
+ * @param ip - Client IP
343
+ * @param now - Current timestamp
344
+ * @returns Block information or null
345
+ */
346
+ getBlockInfo(ip, now = Date.now()) {
347
+ if (!this.isBlocked(ip, now)) {
348
+ return null;
349
+ }
350
+ const cleanIp = normalizeIp(ip);
351
+ const entry = this.blocks.get(cleanIp);
352
+ if (!entry) {
353
+ return null;
354
+ }
355
+ return {
356
+ ip: cleanIp,
357
+ blockedAt: entry.blockedAt,
358
+ expiresAt: entry.expiresAt,
359
+ remainingMs: Math.max(0, entry.expiresAt - now),
360
+ reason: entry.reason
361
+ };
362
+ }
363
+ /**
364
+ * Returns a list of all currently active blocked IPs.
365
+ *
366
+ * @param now - Current timestamp
367
+ * @returns Array of active block records
368
+ */
369
+ listBlocks(now = Date.now()) {
370
+ this.pruneExpired(now);
371
+ const result = [];
372
+ for (const [ip, entry] of this.blocks.entries()) {
373
+ result.push({
374
+ ip,
375
+ blockedAt: entry.blockedAt,
376
+ expiresAt: entry.expiresAt,
377
+ remainingMs: Math.max(0, entry.expiresAt - now),
378
+ reason: entry.reason
379
+ });
380
+ }
381
+ return result;
382
+ }
383
+ /**
384
+ * Scans and purges any expired blocks from memory.
385
+ *
386
+ * @param now - Current timestamp
387
+ * @returns Number of purged blocks
388
+ */
389
+ pruneExpired(now = Date.now()) {
390
+ let pruned = 0;
391
+ for (const [ip, entry] of this.blocks.entries()) {
392
+ if (now >= entry.expiresAt) {
393
+ this.blocks.delete(ip);
394
+ pruned++;
395
+ }
396
+ }
397
+ return pruned;
398
+ }
399
+ /**
400
+ * Clears all blocks from memory.
401
+ */
402
+ clear() {
403
+ this.blocks.clear();
404
+ }
405
+ /**
406
+ * Number of IPs currently stored in memory.
407
+ */
408
+ get size() {
409
+ return this.blocks.size;
410
+ }
411
+ /**
412
+ * Evicts the oldest entry to strictly uphold maxBlockedIps limit.
413
+ *
414
+ * @param _now - Current timestamp
415
+ */
416
+ _evictOldest(_now) {
417
+ const iterator = this.blocks.keys();
418
+ const firstKey = iterator.next().value;
419
+ if (firstKey !== undefined) {
420
+ this.blocks.delete(firstKey);
421
+ }
422
+ }
423
+ }
424
+ exports.BlockManager = BlockManager;
425
+ //# sourceMappingURL=block-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-manager.js","sourceRoot":"","sources":["../src/block-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import net from 'node:net';\nimport type { BlockRecord, ZShieldConfig, ZShieldOptions } from './types';\n\nexport interface BlockManagerOptions {\n blockDurationMs?: number;\n maxBlockedIps?: number;\n}\n\nexport interface BlockInfo {\n ip: string;\n blockedAt: number;\n expiresAt: number;\n remainingMs: number;\n reason: string;\n}\n\ninterface InternalBlockEntry {\n blockedAt: number;\n expiresAt: number;\n reason: string;\n}\n\n/**\n * Canonicalizes an IPv6 address string according to RFC 5952.\n * - Parses 16-bit hextets\n * - Handles embedded IPv4 in IPv6 (including IPv4-mapped IPv6)\n * - Normalizes loopback ('::1' and '0:0:0:0:0:0:0:1') to '127.0.0.1'\n * - Normalizes IPv4-mapped addresses to standard IPv4\n * - Compresses the longest run of 2 or more zeros with '::' (first run on tie)\n * - Lowers all hexadecimal characters and removes leading zeros in each field\n *\n * @param ip - Valid IPv6 address string\n * @returns Canonicalized representation\n */\nexport function canonicalizeIpv6(ip: string): string {\n let str = ip.toLowerCase().trim();\n const zoneIndex = str.indexOf('%');\n if (zoneIndex !== -1) {\n str = str.substring(0, zoneIndex);\n }\n\n // Handle embedded IPv4 (e.g. ::ffff:192.0.2.1)\n let embeddedIpv4: number[] | null = null;\n const lastColon = str.lastIndexOf(':');\n if (lastColon !== -1) {\n const candidateIpv4 = str.substring(lastColon + 1);\n if (candidateIpv4.includes('.')) {\n if (net.isIP(candidateIpv4) === 4) {\n embeddedIpv4 = candidateIpv4.split('.').map(Number);\n str = str.substring(0, lastColon) + ':0:0';\n } else {\n return ip.toLowerCase().trim();\n }\n }\n }\n\n const parts = str.split('::');\n if (parts.length > 2) {\n return ip.toLowerCase().trim();\n }\n\n const head = parts[0] ? parts[0].split(':').filter(Boolean) : [];\n const tail = parts.length === 2 && parts[1] ? parts[1].split(':').filter(Boolean) : [];\n\n const totalHextets = head.length + tail.length;\n if (parts.length === 1 && totalHextets !== 8) {\n return ip.toLowerCase().trim();\n }\n if (parts.length === 2 && totalHextets >= 8) {\n return ip.toLowerCase().trim();\n }\n\n const missingZeros = 8 - totalHextets;\n const hextets: number[] = [];\n\n for (const h of head) {\n const val = parseInt(h, 16);\n if (isNaN(val) || val < 0 || val > 0xffff) {\n return ip.toLowerCase().trim();\n }\n hextets.push(val);\n }\n for (let i = 0; i < missingZeros; i++) {\n hextets.push(0);\n }\n for (const t of tail) {\n const val = parseInt(t, 16);\n if (isNaN(val) || val < 0 || val > 0xffff) {\n return ip.toLowerCase().trim();\n }\n hextets.push(val);\n }\n\n if (hextets.length !== 8) {\n return ip.toLowerCase().trim();\n }\n\n if (embeddedIpv4 && embeddedIpv4.length === 4) {\n hextets[6] = ((embeddedIpv4[0] ?? 0) << 8) | (embeddedIpv4[1] ?? 0);\n hextets[7] = ((embeddedIpv4[2] ?? 0) << 8) | (embeddedIpv4[3] ?? 0);\n }\n\n // IPv4-mapped IPv6: ::ffff:w.x.y.z or 0:0:0:0:0:ffff:w.x.y.z\n const isIpv4Mapped =\n hextets[0] === 0 &&\n hextets[1] === 0 &&\n hextets[2] === 0 &&\n hextets[3] === 0 &&\n hextets[4] === 0 &&\n hextets[5] === 0xffff;\n if (isIpv4Mapped) {\n const h6 = hextets[6] ?? 0;\n const h7 = hextets[7] ?? 0;\n const b0 = (h6 >> 8) & 0xff;\n const b1 = h6 & 0xff;\n const b2 = (h7 >> 8) & 0xff;\n const b3 = h7 & 0xff;\n return `${b0}.${b1}.${b2}.${b3}`;\n }\n\n // IPv6 Loopback: ::1 or 0:0:0:0:0:0:0:1 -> 127.0.0.1\n const isLoopback = hextets.slice(0, 7).every((h) => h === 0) && hextets[7] === 1;\n if (isLoopback) {\n return '127.0.0.1';\n }\n\n // Find longest contiguous run of zeros (length >= 2) under RFC 5952\n let bestStart = -1;\n let bestLen = 0;\n let curStart = -1;\n let curLen = 0;\n\n for (let i = 0; i < 8; i++) {\n if (hextets[i] === 0) {\n if (curStart === -1) {\n curStart = i;\n curLen = 1;\n } else {\n curLen++;\n }\n if (curLen > bestLen) {\n bestStart = curStart;\n bestLen = curLen;\n }\n } else {\n curStart = -1;\n curLen = 0;\n }\n }\n\n // If no zero sequence of 2 or more hextets exists, join all 8 hextets\n if (bestLen < 2) {\n return hextets.map((h) => h.toString(16)).join(':');\n }\n\n const headStr = hextets.slice(0, bestStart).map((h) => h.toString(16)).join(':');\n const tailStr = hextets.slice(bestStart + bestLen).map((h) => h.toString(16)).join(':');\n return `${headStr}::${tailStr}`;\n}\n\n/**\n * Normalizes an IP address for consistent comparison across IPv4 and IPv6.\n * - Trims whitespace\n * - Canonicalizes IPv6 addresses according to RFC 5952 (zero compression, lowercase)\n * - Unmaps all IPv4-mapped IPv6 variations (e.g. '::ffff:192.168.1.1' -> '192.168.1.1')\n * - Normalizes all IPv6 loopback variants ('::1', '0:0:0:0:0:0:0:1') to '127.0.0.1'\n * - Preserves invalid strings without converting them into fake valid IPs\n *\n * @param ip - Raw client IP candidate\n * @returns Normalized IP or 'unknown'\n */\nexport function normalizeIp(ip: unknown): string {\n if (typeof ip !== 'string') {\n return 'unknown';\n }\n const trimmed = ip.trim();\n if (trimmed.length === 0) {\n return 'unknown';\n }\n\n // Standard valid IPv4\n const ipType = net.isIP(trimmed);\n if (ipType === 4) {\n return trimmed;\n }\n\n // Standard valid IPv6 (including loopback and IPv4-mapped)\n if (ipType === 6) {\n return canonicalizeIpv6(trimmed);\n }\n\n // Handle possible ::ffff:192.0.2.1 where net.isIP might return 0\n const lower = trimmed.toLowerCase();\n if (lower.startsWith('::ffff:')) {\n const candidateIpv4 = trimmed.substring(7);\n if (net.isIP(candidateIpv4) === 4) {\n return candidateIpv4;\n }\n }\n\n return trimmed;\n}\n\n/**\n * Validates whether an IP address is a valid IPv4 or IPv6 string.\n * Uses Node.js built-in net.isIP (returns 4 for IPv4, 6 for IPv6, 0 for invalid).\n *\n * @param ip - IP candidate string\n * @returns True if valid IPv4 or IPv6\n */\nexport function isValidIp(ip: unknown): boolean {\n if (typeof ip !== 'string' || ip.trim().length === 0) {\n return false;\n }\n const norm = normalizeIp(ip);\n return net.isIP(norm) !== 0;\n}\n\n/**\n * Parses block duration into milliseconds.\n * Supports numbers (milliseconds) or human-readable strings ('30s', '10m', '1h', '2d').\n *\n * @param duration - Numeric ms or formatted string\n * @returns Duration in milliseconds\n */\nexport function parseDuration(duration: number | string): number {\n if (typeof duration === 'number') {\n const ms = Math.floor(duration);\n if (!Number.isFinite(duration) || duration <= 0 || ms <= 0) {\n throw new TypeError('[Z-Shield] Block duration must be a positive number in milliseconds.');\n }\n return ms;\n }\n\n if (typeof duration === 'string') {\n const trimmed = duration.trim();\n const match = trimmed.match(/^(-?\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)?$/i);\n if (!match) {\n throw new Error(`[Z-Shield] Invalid duration format: \"${duration}\". Use format like \"10m\", \"1h\", \"30s\", or milliseconds.`);\n }\n const rawVal = match[1];\n if (rawVal === undefined) {\n throw new Error('[Z-Shield] Block duration must be greater than zero.');\n }\n const val = parseFloat(rawVal);\n if (!Number.isFinite(val) || val <= 0) {\n throw new Error('[Z-Shield] Block duration must be greater than zero.');\n }\n const unit = (match[2] || 'ms').toLowerCase();\n let ms: number;\n switch (unit) {\n case 'ms': ms = Math.floor(val); break;\n case 's': ms = Math.floor(val * 1000); break;\n case 'm': ms = Math.floor(val * 60 * 1000); break;\n case 'h': ms = Math.floor(val * 60 * 60 * 1000); break;\n case 'd': ms = Math.floor(val * 24 * 60 * 60 * 1000); break;\n default: ms = Math.floor(val); break;\n }\n if (ms <= 0) {\n throw new Error('[Z-Shield] Block duration must be greater than zero.');\n }\n return ms;\n }\n\n throw new TypeError('[Z-Shield] Block duration must be a number (milliseconds) or a duration string like \"10m\".');\n}\n\n/**\n * BlockManager - In-Memory Temporary IP Block Management\n *\n * Characteristics:\n * - Process-Local: Blocks are held in RAM. They do NOT survive server restarts.\n * - Automatic Expiry: Block entries are checked against their expiration timestamp.\n * - Bounded Memory: Uses LRU eviction when `maxBlockedIps` is reached to prevent memory exhaustion.\n * - No Background Timers: Uses passive/lazy expiration on access to keep Node.js event loop idle.\n */\nexport class BlockManager {\n readonly defaultDurationMs: number;\n readonly maxBlockedIps: number;\n private readonly blocks: Map<string, InternalBlockEntry>;\n\n /**\n * @param options - Block manager options or resolved config\n */\n constructor(options: BlockManagerOptions | ZShieldConfig | ZShieldOptions = {}) {\n this.defaultDurationMs = options.blockDurationMs || 15 * 60 * 1000;\n this.maxBlockedIps = options.maxBlockedIps || 5000;\n\n this.blocks = new Map<string, InternalBlockEntry>();\n }\n\n /**\n * Temporarily blocks an IP address.\n *\n * @param ip - Valid IPv4 or IPv6 address\n * @param duration - Duration in ms or string ('10m')\n * @param reason - Reason for block\n * @param now - Current timestamp (injectable for testing)\n * @returns Created block record\n */\n block(\n ip: string,\n duration: number | string = this.defaultDurationMs,\n reason: string = 'Administrative block',\n now: number = Date.now()\n ): BlockRecord {\n if (!isValidIp(ip)) {\n throw new TypeError(`[Z-Shield] Cannot block invalid IP address: \"${ip}\".`);\n }\n\n const durationMs = parseDuration(duration);\n const cleanIp = normalizeIp(ip);\n\n // Bound memory: if at capacity, evict an old entry before inserting\n if (this.blocks.size >= this.maxBlockedIps && !this.blocks.has(cleanIp)) {\n this._evictOldest(now);\n }\n\n const entry: InternalBlockEntry = {\n blockedAt: now,\n expiresAt: now + durationMs,\n reason: String(reason || 'Administrative block')\n };\n\n // Refresh LRU order: delete if exists, then set\n this.blocks.delete(cleanIp);\n this.blocks.set(cleanIp, entry);\n\n return {\n ip: cleanIp,\n blockedAt: entry.blockedAt,\n expiresAt: entry.expiresAt,\n durationMs,\n reason: entry.reason\n };\n }\n\n /**\n * Manually unblocks an IP address.\n *\n * @param ip - Client IP\n * @returns True if IP was unblocked, false if it was not blocked\n */\n unblock(ip: string): boolean {\n if (!isValidIp(ip)) {\n throw new TypeError(`[Z-Shield] Cannot unblock invalid IP address: \"${ip}\".`);\n }\n return this.blocks.delete(normalizeIp(ip));\n }\n\n /**\n * Checks whether an IP address is currently blocked.\n * Expired blocks are lazily pruned.\n *\n * @param ip - Client IP\n * @param now - Current timestamp\n * @returns True if actively blocked\n */\n isBlocked(ip: string | undefined | null, now: number = Date.now()): boolean {\n if (!ip || typeof ip !== 'string') {\n return false;\n }\n\n const cleanIp = normalizeIp(ip);\n const entry = this.blocks.get(cleanIp);\n\n if (!entry) {\n return false;\n }\n\n // Lazy expiration check\n if (now >= entry.expiresAt) {\n this.blocks.delete(cleanIp);\n return false;\n }\n\n return true;\n }\n\n /**\n * Retrieves active block details for an IP, or null if not blocked/expired.\n *\n * @param ip - Client IP\n * @param now - Current timestamp\n * @returns Block information or null\n */\n getBlockInfo(ip: string, now: number = Date.now()): BlockInfo | null {\n if (!this.isBlocked(ip, now)) {\n return null;\n }\n\n const cleanIp = normalizeIp(ip);\n const entry = this.blocks.get(cleanIp);\n if (!entry) {\n return null;\n }\n\n return {\n ip: cleanIp,\n blockedAt: entry.blockedAt,\n expiresAt: entry.expiresAt,\n remainingMs: Math.max(0, entry.expiresAt - now),\n reason: entry.reason\n };\n }\n\n /**\n * Returns a list of all currently active blocked IPs.\n *\n * @param now - Current timestamp\n * @returns Array of active block records\n */\n listBlocks(now: number = Date.now()): BlockInfo[] {\n this.pruneExpired(now);\n const result: BlockInfo[] = [];\n for (const [ip, entry] of this.blocks.entries()) {\n result.push({\n ip,\n blockedAt: entry.blockedAt,\n expiresAt: entry.expiresAt,\n remainingMs: Math.max(0, entry.expiresAt - now),\n reason: entry.reason\n });\n }\n return result;\n }\n\n /**\n * Scans and purges any expired blocks from memory.\n *\n * @param now - Current timestamp\n * @returns Number of purged blocks\n */\n pruneExpired(now: number = Date.now()): number {\n let pruned = 0;\n for (const [ip, entry] of this.blocks.entries()) {\n if (now >= entry.expiresAt) {\n this.blocks.delete(ip);\n pruned++;\n }\n }\n return pruned;\n }\n\n /**\n * Clears all blocks from memory.\n */\n clear(): void {\n this.blocks.clear();\n }\n\n /**\n * Number of IPs currently stored in memory.\n */\n get size(): number {\n return this.blocks.size;\n }\n\n /**\n * Evicts the oldest entry to strictly uphold maxBlockedIps limit.\n *\n * @param _now - Current timestamp\n */\n private _evictOldest(_now: number): void {\n const iterator = this.blocks.keys();\n const firstKey = iterator.next().value;\n\n if (firstKey !== undefined) {\n this.blocks.delete(firstKey);\n }\n }\n}\n"]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ export interface ParsedCliArgs {
3
+ command: string;
4
+ args: string[];
5
+ options: Record<string, string | boolean>;
6
+ }
7
+ export type ColorFormatter = (str: string | number | undefined | null) => string;
8
+ export interface ColorMap {
9
+ bold: ColorFormatter;
10
+ dim: ColorFormatter;
11
+ italic: ColorFormatter;
12
+ underline: ColorFormatter;
13
+ inverse: ColorFormatter;
14
+ red: ColorFormatter;
15
+ green: ColorFormatter;
16
+ yellow: ColorFormatter;
17
+ blue: ColorFormatter;
18
+ magenta: ColorFormatter;
19
+ cyan: ColorFormatter;
20
+ white: ColorFormatter;
21
+ gray: ColorFormatter;
22
+ enabled: boolean;
23
+ hasColors: boolean;
24
+ }
25
+ export interface CliOutput {
26
+ log: (...args: unknown[]) => void;
27
+ error: (...args: unknown[]) => void;
28
+ isTTY?: boolean;
29
+ }
30
+ export interface LogReadResult {
31
+ events: Record<string, unknown>[];
32
+ totalLines: number;
33
+ malformedCount: number;
34
+ fileExists: boolean;
35
+ }
36
+ export interface StatusResult {
37
+ fileExists: boolean;
38
+ totalEvents: number;
39
+ malformedCount?: number;
40
+ typeCounts?: Record<string, number>;
41
+ actionCounts?: Record<string, number>;
42
+ topIps?: Array<{
43
+ ip: string;
44
+ count: number;
45
+ }>;
46
+ topRoutes?: Array<{
47
+ route: string;
48
+ count: number;
49
+ }>;
50
+ }
51
+ /**
52
+ * Parses raw command-line arguments into a structured command object.
53
+ *
54
+ * @param argv - Array of CLI arguments (typically process.argv.slice(2))
55
+ * @returns Structured CLI command, positional args, and parsed options
56
+ */
57
+ export declare function parseCliArgs(argv?: string[]): ParsedCliArgs;
58
+ /**
59
+ * Resolves the target JSON Lines log file path based on CLI options or defaults.
60
+ *
61
+ * @param options - Parsed CLI options
62
+ * @returns Absolute path to events.jsonl
63
+ */
64
+ export declare function resolveLogPath(options?: Record<string, unknown>): string;
65
+ /**
66
+ * Creates an ANSI color styling helper object.
67
+ * When enabled is false, all styling methods act as plain passthroughs.
68
+ *
69
+ * @param enabled - Whether color styling is active
70
+ * @returns Color styling map
71
+ */
72
+ export declare function createColors(enabled?: boolean): ColorMap;
73
+ /**
74
+ * Determines whether terminal colors should be enabled based on
75
+ * explicit options, environment variables, and TTY capabilities.
76
+ *
77
+ * Respects the standard NO_COLOR specification (https://no-color.org).
78
+ *
79
+ * @param options - Parsed CLI options
80
+ * @param output - Output destination (console or mock stream)
81
+ * @returns True if colors should be enabled
82
+ */
83
+ export declare function resolveColorSupport(options?: Record<string, unknown>, output?: CliOutput | Console | null): boolean;
84
+ /**
85
+ * Formats a single security event record into a clean, sanitized terminal line.
86
+ * Strips all ANSI escapes and control characters from untrusted log fields before formatting.
87
+ *
88
+ * @param event - Security event object
89
+ * @param options - Options object or boolean for color
90
+ * @returns Sanitized output line
91
+ */
92
+ export declare function formatLogEvent(event: Record<string, unknown> | null | undefined, options?: Record<string, unknown> | boolean): string;
93
+ /**
94
+ * Reads and parses the last N records from the JSON Lines file.
95
+ * Handles missing files and skips malformed records gracefully.
96
+ *
97
+ * @param logFilePath - Path to log file
98
+ * @param limit - Max number of events to return
99
+ * @returns Extracted events and counts
100
+ */
101
+ export declare function readLogs(logFilePath: string, limit?: number): LogReadResult;
102
+ /**
103
+ * Sanitizes untrusted IP address values from logs for safe status aggregation and output.
104
+ * Strips ANSI escapes, truncates at newlines/control characters, and extracts valid IP addresses.
105
+ *
106
+ * @param rawIp - Untrusted IP candidate from logs
107
+ * @returns Normalized IP address or 'unknown'
108
+ */
109
+ export declare function sanitizeIp(rawIp: unknown): string;
110
+ /**
111
+ * Sanitizes untrusted event type strings from logs.
112
+ *
113
+ * @param rawType - Untrusted event type string
114
+ * @param maxLength - Maximum permitted length
115
+ * @returns Clean event type string
116
+ */
117
+ export declare function sanitizeType(rawType: unknown, maxLength?: number): string;
118
+ /**
119
+ * Sanitizes untrusted action strings from logs.
120
+ *
121
+ * @param rawAction - Untrusted action string
122
+ * @param maxLength - Maximum permitted length
123
+ * @returns Clean action string
124
+ */
125
+ export declare function sanitizeAction(rawAction: unknown, maxLength?: number): string;
126
+ /**
127
+ * Computes security summary statistics from the log file.
128
+ *
129
+ * @param logFilePath - Path to log file
130
+ * @returns Summary metrics
131
+ */
132
+ export declare function calculateStatus(logFilePath: string): StatusResult;
133
+ /**
134
+ * Main command runner. Returns an exit code (0 for success, 1 for error).
135
+ *
136
+ * @param argv - Arguments array
137
+ * @param output - Output destination (console or mock object)
138
+ * @returns Exit code
139
+ */
140
+ export declare function runCommand(argv?: string[], output?: CliOutput | Console): Promise<number>;
141
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}