z-shield 0.1.1 → 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 -764
  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 -280
  51. package/src/middleware.js +0 -288
  52. package/src/rate-limiter.js +0 -186
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🛡️ Z-Shield
2
2
 
3
- > Lightweight, zero-dependency API abuse detection, rate limiting, and security event logging toolkit for Node.js and Express backends.
3
+ > Lightweight, zero-dependency API abuse detection, rate limiting, and security event logging toolkit for Node.js and Express backends by Zeesu Royalist.
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
6
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18.0.0-green.svg)](https://nodejs.org)
@@ -15,6 +15,7 @@
15
15
  - [What Z-Shield Does NOT Do](#what-z-shield-does-not-do)
16
16
  - [Requirements & Installation](#requirements--installation)
17
17
  - [Quick Start: Express Integration](#quick-start-express-integration)
18
+ - [Quick Start: Next.js App Router Integration](#quick-start-nextjs-app-router-integration)
18
19
  - [Monitor Mode vs. Protect Mode](#monitor-mode-vs-protect-mode)
19
20
  - [Configuration Reference](#configuration-reference)
20
21
  - [Rate Limiting & HTTP Headers](#rate-limiting--http-headers)
@@ -114,6 +115,51 @@ app.listen(3000, () => {
114
115
 
115
116
  ---
116
117
 
118
+ ## Quick Start: Next.js App Router Integration
119
+
120
+ Z-Shield provides native compatibility for Next.js App Router route handlers (`app/api/.../route.ts`) using Web-standard `Request` and `Response` objects.
121
+
122
+ ```typescript
123
+ // app/api/protected/route.ts
124
+ import { withNextShield } from 'z-shield';
125
+
126
+ // CRITICAL: Z-Shield requires the Node.js runtime for file-based event logging.
127
+ // It cannot run in the Edge runtime.
128
+ export const runtime = 'nodejs';
129
+
130
+ export const GET = withNextShield(
131
+ async (request: Request) => {
132
+ return Response.json({
133
+ success: true,
134
+ message: 'Access granted to protected Next.js API route'
135
+ });
136
+ },
137
+ {
138
+ mode: 'protect',
139
+ maxRequests: 50,
140
+ windowMs: 60000,
141
+ // Enable trustProxy and select your trusted reverse-proxy strategy
142
+ trustProxy: true,
143
+ getIp: 'cloudflare', // 'cloudflare' | 'x-real-ip' | 'x-forwarded-for' | 'akamai' | (req) => ...
144
+ appName: 'my-nextjs-app'
145
+ }
146
+ );
147
+ ```
148
+
149
+ ### Next.js Architectural Notes
150
+ - **Node.js Runtime Requirement**: Z-Shield relies on Node.js filesystem APIs for non-blocking JSONL event logging and `node:net` for strict IP validation. Always declare `export const runtime = 'nodejs';`.
151
+ - **Trusted Proxies & `getIp` Strategy**:
152
+ When deploying behind reverse proxies, set `trustProxy: true` AND explicitly specify your provider's extraction strategy using `getIp`:
153
+ - `'cloudflare'` / `'cf-connecting-ip'`: Inspects only `CF-Connecting-IP`.
154
+ - `'nginx'` / `'x-real-ip'`: Inspects only `X-Real-IP`.
155
+ - `'akamai'` / `'true-client-ip'`: Inspects only `True-Client-IP`.
156
+ - `'x-forwarded-for'`: Inspects the first valid IP from `X-Forwarded-For` (requires outermost reverse proxy to strip or sanitize client-provided headers).
157
+ - Custom function: `getIp: (req) => string | null | undefined`.
158
+ *Fail-Safe Behavior*: If `trustProxy: true` is configured without specifying a `getIp` strategy, Z-Shield strictly refuses to guess across foreign proxy headers and safely falls back to `'unknown'` (or direct TCP socket if available). Unconfigured reverse-proxy headers and unverified platform properties are never trusted.
159
+ - **Process-Local Memory in Serverless**: Rate limit counters and temporary IP blocks are stored in-memory per Node.js process (bounded by LRU). In serverless environments, each active instance tracks traffic locally.
160
+
161
+ ---
162
+
117
163
  ## Monitor Mode vs. Protect Mode
118
164
 
119
165
  Z-Shield provides two operational modes:
@@ -172,12 +218,16 @@ const shield = createShield({
172
218
  autoBlock: false,
173
219
 
174
220
  // Whether to trust reverse-proxy headers (e.g. X-Forwarded-For)
175
- // WARNING: Set to true only if behind a verified reverse proxy
221
+ // WARNING: Set to true only if behind a verified reverse proxy.
222
+ // In Next.js App Router, also pair with 'getIp' strategy (e.g. 'cloudflare', 'nginx', 'x-forwarded-for')
176
223
  trustProxy: false,
177
224
 
178
225
  // Array of route prefixes that bypass tracking
179
226
  excludeRoutes: ['/health', '/favicon.ico'],
180
227
 
228
+ // Maximum number of pending asynchronous log writes allowed in memory before dropping
229
+ maxLogQueueSize: 1000,
230
+
181
231
  // Optional custom error callback for failed log writes: (err, event) => void
182
232
  onLogError: null
183
233
  });
@@ -300,13 +350,31 @@ npx z-shield block 198.51.100.1 --duration 10m
300
350
  npx z-shield unblock 198.51.100.1
301
351
  ```
302
352
 
353
+ ### Log Directory Resolution & Environment Variables
354
+ By default, the CLI looks for logs in `.z-shield-logs/events.jsonl` in your current working directory. You can customize the log directory using either the CLI flag or environment variable:
355
+
356
+ 1. **CLI Flag (`--log-dir`)** (Highest precedence):
357
+ ```bash
358
+ npx z-shield status --log-dir /var/log/my-app
359
+ npx z-shield logs --log-dir /var/log/my-app
360
+ ```
361
+ 2. **Environment Variable (`Z_SHIELD_LOG_DIR`)** (Second precedence):
362
+ ```bash
363
+ export Z_SHIELD_LOG_DIR=/var/log/my-app
364
+ npx z-shield status
365
+ ```
366
+ 3. **Default Location** (Fallback):
367
+ `./.z-shield-logs/events.jsonl`
368
+
369
+ When no log file is found, the CLI prints a helpful status message with the checked path and guidance on how to run your application or specify custom paths.
370
+
303
371
  ### Process-Local Architecture Notice
304
- Z-Shield stores active IP counters and temporary blocks **in server memory (RAM)** inside your Express application process.
372
+ Z-Shield stores active IP counters and temporary blocks **in server memory (RAM)** inside your Express or Next.js application process.
305
373
 
306
- Because the CLI runs in an independent Node.js process, it cannot reach into your Express application's memory heap to alter active blocks. When you execute `npx z-shield block`, the CLI:
374
+ Because the CLI runs in an independent Node.js process, it cannot reach into your running application's memory heap to alter active in-memory counters. When you execute `npx z-shield block`, the CLI:
307
375
  1. Validates the IP and duration.
308
- 2. Writes an audit entry to `.z-shield-logs/events.jsonl`.
309
- 3. Displays the exact JavaScript snippet to invoke inside your Express code (`shield.block(...)`).
376
+ 2. Writes an audit entry to the active security log file.
377
+ 3. Displays the exact programmatic snippet to invoke inside your application code (`shield.block(...)`).
310
378
 
311
379
  ---
312
380
 
@@ -336,6 +404,55 @@ const shield = createShield({
336
404
  app.use(shield.middleware);
337
405
  ```
338
406
 
407
+ ### Next.js Client IP Extraction & Reverse Proxy Requirements
408
+ In Next.js App Router route handlers, standard Web `Request` objects do not carry Node.js TCP sockets. Z-Shield enforces the following security model:
409
+
410
+ 1. **`trustProxy: false` (Default)**:
411
+ - Untrusted reverse-proxy headers (`X-Forwarded-For`, `X-Real-IP`, `CF-Connecting-IP`, `True-Client-IP`, etc.) and unverified platform properties are strictly ignored to prevent client IP spoofing attacks.
412
+ - If a direct Node.js TCP socket is available (`req.socket.remoteAddress`), it is used.
413
+ - **Deployment Limitation**: In environments without a direct TCP socket (e.g. serverless functions without trusted proxies), safe client IP identification cannot be guaranteed, and Z-Shield safely falls back to `'unknown'`.
414
+
415
+ 2. **`trustProxy: true`**:
416
+ - To prevent forged header injection across reverse proxies (e.g. an attacker sending `CF-Connecting-IP` to an Nginx server that passes unmanaged headers through), Z-Shield **strictly requires selecting an explicit trusted proxy strategy via `getIp`** (or providing a custom function):
417
+ - `'cloudflare'` | `'cf-connecting-ip'`: Inspects only `CF-Connecting-IP` and strictly ignores all other proxy headers.
418
+ - `'nginx'` | `'x-real-ip'`: Inspects only `X-Real-IP` and strictly ignores all other proxy headers.
419
+ - `'akamai'` | `'true-client-ip'`: Inspects only `True-Client-IP` and strictly ignores all other proxy headers.
420
+ - `'x-forwarded-for'`: Inspects only `X-Forwarded-For`, parsing candidates and taking the first valid IP.
421
+ - Custom extractor: `(req: Request) => string | null | undefined`.
422
+ - **Crucial Reverse-Proxy Requirement**: When using `'x-forwarded-for'` with an appending reverse proxy (such as default Nginx `$proxy_add_x_forwarded_for`), your edge proxy **MUST** strip client-supplied `X-Forwarded-For` headers at network ingress; otherwise, an attacker can prepend a forged IP. If your proxy cannot strip incoming headers, configure `'x-real-ip'` or provide a custom `getIp` function.
423
+ - **Fail-Safe Behavior**: If `trustProxy: true` is configured without specifying `getIp`, Z-Shield strictly refuses to guess which header is authentic, and safely falls back to `'unknown'` (or direct TCP socket if available). Unconfigured reverse-proxy headers and unverified platform properties are never trusted.
424
+
425
+ > [!WARNING]
426
+ > **CRITICAL: The Serverless `'unknown'` Shared Bucket Trap**
427
+ >
428
+ > In serverless environments (Vercel Serverless Functions, AWS Lambda, Cloudflare Pages), requests arrive without an underlying direct Node.js TCP socket.
429
+ >
430
+ > If you leave `trustProxy: false` (or set `trustProxy: true` without a valid `getIp`), all incoming client IPs evaluate to `'unknown'`. Consequently, **all global visitors share one single rate-limit counter**. If `maxRequests` is exceeded or `autoBlock: true` triggers on `'unknown'`, **EVERY visitor worldwide will receive HTTP 429**, causing a self-inflicted Denial of Service (DoS) of your entire serverless API.
431
+ >
432
+ > **Remedy**: Always set `trustProxy: true` and specify the `getIp` strategy matching your hosting infrastructure:
433
+ >
434
+ > ```typescript
435
+ > // Vercel: Vercel edge strips/sanitizes X-Forwarded-For at ingress
436
+ > getIp: 'x-forwarded-for' // or 'x-real-ip'
437
+ >
438
+ > // Cloudflare: When application is strictly behind Cloudflare
439
+ > getIp: 'cloudflare' // strictly inspects CF-Connecting-IP
440
+ >
441
+ > // AWS CloudFront / ALB:
442
+ > getIp: (req) => req.headers.get('cloudfront-viewer-address')?.split(':')[0]
443
+ > || req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
444
+ >
445
+ > // Nginx Reverse Proxy (configured with proxy_set_header X-Real-IP $remote_addr):
446
+ > getIp: 'nginx' // strictly inspects X-Real-IP
447
+ > ```
448
+
449
+ > [!CAUTION]
450
+ > **Proxy Headers Are Trustworthy ONLY When Your Infrastructure Sanitizes Them**
451
+ >
452
+ > **No single proxy strategy is safe for every hosting provider!** HTTP headers (`CF-Connecting-IP`, `X-Real-IP`, `X-Forwarded-For`, `True-Client-IP`) are untrusted client metadata unless your hosting proxy explicitly sets or sanitizes them.
453
+ >
454
+ > Never configure a strategy that does not match your actual ingress infrastructure (e.g. configuring `getIp: 'cloudflare'` on AWS or Bare Metal without Cloudflare allows anyone on the internet to send a forged `CF-Connecting-IP` header). Furthermore, always ensure direct origin access is firewalled so traffic cannot bypass your proxy.
455
+
339
456
  ---
340
457
 
341
458
  ## Known Limitations
@@ -0,0 +1,134 @@
1
+ import type { BlockRecord, ZShieldConfig, ZShieldOptions } from './types';
2
+ export interface BlockManagerOptions {
3
+ blockDurationMs?: number;
4
+ maxBlockedIps?: number;
5
+ }
6
+ export interface BlockInfo {
7
+ ip: string;
8
+ blockedAt: number;
9
+ expiresAt: number;
10
+ remainingMs: number;
11
+ reason: string;
12
+ }
13
+ /**
14
+ * Canonicalizes an IPv6 address string according to RFC 5952.
15
+ * - Parses 16-bit hextets
16
+ * - Handles embedded IPv4 in IPv6 (including IPv4-mapped IPv6)
17
+ * - Normalizes loopback ('::1' and '0:0:0:0:0:0:0:1') to '127.0.0.1'
18
+ * - Normalizes IPv4-mapped addresses to standard IPv4
19
+ * - Compresses the longest run of 2 or more zeros with '::' (first run on tie)
20
+ * - Lowers all hexadecimal characters and removes leading zeros in each field
21
+ *
22
+ * @param ip - Valid IPv6 address string
23
+ * @returns Canonicalized representation
24
+ */
25
+ export declare function canonicalizeIpv6(ip: string): string;
26
+ /**
27
+ * Normalizes an IP address for consistent comparison across IPv4 and IPv6.
28
+ * - Trims whitespace
29
+ * - Canonicalizes IPv6 addresses according to RFC 5952 (zero compression, lowercase)
30
+ * - Unmaps all IPv4-mapped IPv6 variations (e.g. '::ffff:192.168.1.1' -> '192.168.1.1')
31
+ * - Normalizes all IPv6 loopback variants ('::1', '0:0:0:0:0:0:0:1') to '127.0.0.1'
32
+ * - Preserves invalid strings without converting them into fake valid IPs
33
+ *
34
+ * @param ip - Raw client IP candidate
35
+ * @returns Normalized IP or 'unknown'
36
+ */
37
+ export declare function normalizeIp(ip: unknown): string;
38
+ /**
39
+ * Validates whether an IP address is a valid IPv4 or IPv6 string.
40
+ * Uses Node.js built-in net.isIP (returns 4 for IPv4, 6 for IPv6, 0 for invalid).
41
+ *
42
+ * @param ip - IP candidate string
43
+ * @returns True if valid IPv4 or IPv6
44
+ */
45
+ export declare function isValidIp(ip: unknown): boolean;
46
+ /**
47
+ * Parses block duration into milliseconds.
48
+ * Supports numbers (milliseconds) or human-readable strings ('30s', '10m', '1h', '2d').
49
+ *
50
+ * @param duration - Numeric ms or formatted string
51
+ * @returns Duration in milliseconds
52
+ */
53
+ export declare function parseDuration(duration: number | string): number;
54
+ /**
55
+ * BlockManager - In-Memory Temporary IP Block Management
56
+ *
57
+ * Characteristics:
58
+ * - Process-Local: Blocks are held in RAM. They do NOT survive server restarts.
59
+ * - Automatic Expiry: Block entries are checked against their expiration timestamp.
60
+ * - Bounded Memory: Uses LRU eviction when `maxBlockedIps` is reached to prevent memory exhaustion.
61
+ * - No Background Timers: Uses passive/lazy expiration on access to keep Node.js event loop idle.
62
+ */
63
+ export declare class BlockManager {
64
+ readonly defaultDurationMs: number;
65
+ readonly maxBlockedIps: number;
66
+ private readonly blocks;
67
+ /**
68
+ * @param options - Block manager options or resolved config
69
+ */
70
+ constructor(options?: BlockManagerOptions | ZShieldConfig | ZShieldOptions);
71
+ /**
72
+ * Temporarily blocks an IP address.
73
+ *
74
+ * @param ip - Valid IPv4 or IPv6 address
75
+ * @param duration - Duration in ms or string ('10m')
76
+ * @param reason - Reason for block
77
+ * @param now - Current timestamp (injectable for testing)
78
+ * @returns Created block record
79
+ */
80
+ block(ip: string, duration?: number | string, reason?: string, now?: number): BlockRecord;
81
+ /**
82
+ * Manually unblocks an IP address.
83
+ *
84
+ * @param ip - Client IP
85
+ * @returns True if IP was unblocked, false if it was not blocked
86
+ */
87
+ unblock(ip: string): boolean;
88
+ /**
89
+ * Checks whether an IP address is currently blocked.
90
+ * Expired blocks are lazily pruned.
91
+ *
92
+ * @param ip - Client IP
93
+ * @param now - Current timestamp
94
+ * @returns True if actively blocked
95
+ */
96
+ isBlocked(ip: string | undefined | null, now?: number): boolean;
97
+ /**
98
+ * Retrieves active block details for an IP, or null if not blocked/expired.
99
+ *
100
+ * @param ip - Client IP
101
+ * @param now - Current timestamp
102
+ * @returns Block information or null
103
+ */
104
+ getBlockInfo(ip: string, now?: number): BlockInfo | null;
105
+ /**
106
+ * Returns a list of all currently active blocked IPs.
107
+ *
108
+ * @param now - Current timestamp
109
+ * @returns Array of active block records
110
+ */
111
+ listBlocks(now?: number): BlockInfo[];
112
+ /**
113
+ * Scans and purges any expired blocks from memory.
114
+ *
115
+ * @param now - Current timestamp
116
+ * @returns Number of purged blocks
117
+ */
118
+ pruneExpired(now?: number): number;
119
+ /**
120
+ * Clears all blocks from memory.
121
+ */
122
+ clear(): void;
123
+ /**
124
+ * Number of IPs currently stored in memory.
125
+ */
126
+ get size(): number;
127
+ /**
128
+ * Evicts the oldest entry to strictly uphold maxBlockedIps limit.
129
+ *
130
+ * @param _now - Current timestamp
131
+ */
132
+ private _evictOldest;
133
+ }
134
+ //# sourceMappingURL=block-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-manager.d.ts","sourceRoot":"","sources":["../src/block-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAQD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CA4HnD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CA8B/C;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,OAAO,CAM9C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAwC/D;AAED;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IAEzD;;OAEG;IACH,YAAY,OAAO,GAAE,mBAAmB,GAAG,aAAa,GAAG,cAAmB,EAK7E;IAED;;;;;;;;OAQG;IACH,KAAK,CACH,EAAE,EAAE,MAAM,EACV,QAAQ,GAAE,MAAM,GAAG,MAA+B,EAClD,MAAM,GAAE,MAA+B,EACvC,GAAG,GAAE,MAAmB,GACvB,WAAW,CA8Bb;IAED;;;;;OAKG;IACH,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAK3B;IAED;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EAAE,GAAG,GAAE,MAAmB,GAAG,OAAO,CAmB1E;IAED;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,GAAE,MAAmB,GAAG,SAAS,GAAG,IAAI,CAkBnE;IAED;;;;;OAKG;IACH,UAAU,CAAC,GAAG,GAAE,MAAmB,GAAG,SAAS,EAAE,CAahD;IAED;;;;;OAKG;IACH,YAAY,CAAC,GAAG,GAAE,MAAmB,GAAG,MAAM,CAS7C;IAED;;OAEG;IACH,KAAK,IAAI,IAAI,CAEZ;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;;OAIG;IACH,OAAO,CAAC,YAAY;CAQrB"}