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
package/src/config.js DELETED
@@ -1,165 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
-
5
- /**
6
- * Default configuration values for Z-Shield.
7
- * Designed with conservative, safe defaults.
8
- */
9
- const DEFAULT_CONFIG = {
10
- // Unique identifier for the host application (useful when multiple apps share a server)
11
- appName: 'default-app',
12
-
13
- // Mode: 'monitor' (detect and log only) or 'protect' (enforce 429 and temporary blocks)
14
- mode: 'monitor',
15
-
16
- // Time window for tracking requests in milliseconds (default: 60,000ms = 1 minute)
17
- windowMs: 60 * 1000,
18
-
19
- // Maximum allowed requests from a single IP within the windowMs
20
- maxRequests: 100,
21
-
22
- // Directory where JSONL security event logs will be stored
23
- logDir: path.resolve(process.cwd(), '.z-shield-logs'),
24
-
25
- // Whether to trust reverse-proxy IP headers (e.g., X-Forwarded-For)
26
- // CRITICAL: Must be false by default to prevent client IP spoofing attacks
27
- trustProxy: false,
28
-
29
- // Array of route prefixes that bypass abuse tracking (e.g. internal health checks)
30
- excludeRoutes: ['/health', '/favicon.ico'],
31
-
32
- // Maximum number of distinct client IPs to track in memory simultaneously
33
- // Protects against Memory Exhaustion DoS attacks
34
- maxTrackedIps: 10000,
35
-
36
- // Duration in milliseconds for temporary IP blocks (default: 15 minutes)
37
- blockDurationMs: 15 * 60 * 1000,
38
-
39
- // Maximum number of blocked IPs to keep in memory simultaneously
40
- maxBlockedIps: 5000,
41
-
42
- // Whether exceeding rate limit in protect mode automatically triggers a temporary block
43
- autoBlock: false,
44
-
45
- // Filename for the active JSON Lines log file
46
- logFilename: 'events.jsonl',
47
-
48
- // Maximum size in bytes before rotating the log file (default: 5MB)
49
- maxLogSizeBytes: 5 * 1024 * 1024,
50
-
51
- // Maximum number of rotated log files to retain
52
- maxLogFiles: 5,
53
-
54
- // Optional custom error callback: (err, event) => void
55
- onLogError: null
56
- };
57
-
58
- /**
59
- * Validates user-provided options and merges them with defaults.
60
- * Throws clear, helpful errors if an invalid option is passed.
61
- *
62
- * @param {Object} [userConfig={}] - Custom configuration provided by the developer
63
- * @returns {Readonly<Object>} Validated and frozen configuration object
64
- */
65
- function resolveConfig(userConfig = {}) {
66
- if (userConfig === null || typeof userConfig !== 'object' || Array.isArray(userConfig)) {
67
- throw new TypeError('[Z-Shield] Configuration must be a plain object.');
68
- }
69
-
70
- const config = { ...DEFAULT_CONFIG, ...userConfig };
71
-
72
- // Validate appName
73
- if (typeof config.appName !== 'string' || config.appName.trim().length === 0) {
74
- throw new TypeError('[Z-Shield] "appName" must be a non-empty string.');
75
- }
76
- // Prevent directory traversal characters in appName (e.g., "../hacker")
77
- if (!/^[a-zA-Z0-9_-]+$/.test(config.appName)) {
78
- throw new Error('[Z-Shield] "appName" can only contain letters, numbers, dashes, and underscores.');
79
- }
80
-
81
- // Validate mode
82
- if (config.mode !== 'monitor' && config.mode !== 'protect') {
83
- throw new Error(`[Z-Shield] Invalid mode "${config.mode}". Mode must be either "monitor" or "protect".`);
84
- }
85
-
86
- // Validate windowMs
87
- if (typeof config.windowMs !== 'number' || !Number.isFinite(config.windowMs) || config.windowMs <= 0) {
88
- throw new TypeError('[Z-Shield] "windowMs" must be a positive number in milliseconds.');
89
- }
90
-
91
- // Validate maxRequests
92
- if (typeof config.maxRequests !== 'number' || !Number.isInteger(config.maxRequests) || config.maxRequests <= 0) {
93
- throw new TypeError('[Z-Shield] "maxRequests" must be a positive integer.');
94
- }
95
-
96
- // Validate logDir
97
- if (typeof config.logDir !== 'string' || config.logDir.trim().length === 0) {
98
- throw new TypeError('[Z-Shield] "logDir" must be a non-empty string path.');
99
- }
100
-
101
- // Validate trustProxy
102
- if (typeof config.trustProxy !== 'boolean') {
103
- throw new TypeError('[Z-Shield] "trustProxy" must be a boolean (true or false).');
104
- }
105
-
106
- // Validate excludeRoutes
107
- if (!Array.isArray(config.excludeRoutes) || !config.excludeRoutes.every(r => typeof r === 'string')) {
108
- throw new TypeError('[Z-Shield] "excludeRoutes" must be an array of string route prefixes.');
109
- }
110
-
111
- // Validate maxTrackedIps
112
- if (typeof config.maxTrackedIps !== 'number' || !Number.isInteger(config.maxTrackedIps) || config.maxTrackedIps <= 0) {
113
- throw new TypeError('[Z-Shield] "maxTrackedIps" must be a positive integer.');
114
- }
115
-
116
- // Validate blockDurationMs
117
- if (typeof config.blockDurationMs !== 'number' || !Number.isFinite(config.blockDurationMs) || config.blockDurationMs <= 0) {
118
- throw new TypeError('[Z-Shield] "blockDurationMs" must be a positive number in milliseconds.');
119
- }
120
-
121
- // Validate maxBlockedIps
122
- if (typeof config.maxBlockedIps !== 'number' || !Number.isInteger(config.maxBlockedIps) || config.maxBlockedIps <= 0) {
123
- throw new TypeError('[Z-Shield] "maxBlockedIps" must be a positive integer.');
124
- }
125
-
126
- // Validate autoBlock
127
- if (typeof config.autoBlock !== 'boolean') {
128
- throw new TypeError('[Z-Shield] "autoBlock" must be a boolean (true or false).');
129
- }
130
-
131
- // Validate logFilename
132
- if (typeof config.logFilename !== 'string' || config.logFilename.trim().length === 0) {
133
- throw new TypeError('[Z-Shield] "logFilename" must be a non-empty string.');
134
- }
135
- if (!/^[a-zA-Z0-9._-]+$/.test(config.logFilename) || config.logFilename.includes('..')) {
136
- throw new Error('[Z-Shield] "logFilename" must be a safe filename without path traversal characters.');
137
- }
138
-
139
- // Validate maxLogSizeBytes
140
- if (typeof config.maxLogSizeBytes !== 'number' || !Number.isInteger(config.maxLogSizeBytes) || config.maxLogSizeBytes <= 0) {
141
- throw new TypeError('[Z-Shield] "maxLogSizeBytes" must be a positive integer.');
142
- }
143
-
144
- // Validate maxLogFiles
145
- if (typeof config.maxLogFiles !== 'number' || !Number.isInteger(config.maxLogFiles) || config.maxLogFiles < 0) {
146
- throw new TypeError('[Z-Shield] "maxLogFiles" must be a non-negative integer.');
147
- }
148
-
149
- // Validate onLogError
150
- if (config.onLogError !== null && typeof config.onLogError !== 'function') {
151
- throw new TypeError('[Z-Shield] "onLogError" must be null or a function.');
152
- }
153
-
154
- // Freeze the object so application code cannot accidentally mutate it at runtime
155
- return Object.freeze({
156
- ...config,
157
- logDir: path.resolve(config.logDir),
158
- excludeRoutes: Object.freeze([...config.excludeRoutes])
159
- });
160
- }
161
-
162
- module.exports = {
163
- DEFAULT_CONFIG,
164
- resolveConfig
165
- };
package/src/detector.js DELETED
@@ -1,88 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * Detector - Evaluates incoming requests and detects abuse patterns.
5
- *
6
- * In Phase 2, this evaluates whether a client IP has exceeded the
7
- * maximum request threshold within the configured time window.
8
- */
9
- class Detector {
10
- /**
11
- * @param {Object} options
12
- * @param {import('./rate-limiter').RateLimiter} options.rateLimiter - The rate-limiter instance
13
- * @param {Object} options.config - The resolved Z-Shield configuration
14
- */
15
- constructor({ rateLimiter, config }) {
16
- if (!rateLimiter) {
17
- throw new TypeError('[Z-Shield] Detector requires a RateLimiter instance.');
18
- }
19
- if (!config) {
20
- throw new TypeError('[Z-Shield] Detector requires a resolved config object.');
21
- }
22
-
23
- this.rateLimiter = rateLimiter;
24
- this.config = config;
25
- }
26
-
27
- /**
28
- * Checks an incoming request against detection rules.
29
- *
30
- * @param {string} ip - Client IP address
31
- * @param {import('express').Request} [req={}] - Express request object
32
- * @param {number} [now=Date.now()] - Timestamp
33
- * @returns {Object} Detection evaluation result
34
- */
35
- check(ip, req = {}, now = Date.now()) {
36
- const status = this.rateLimiter.record(ip, now);
37
-
38
- if (status.isExceeded) {
39
- const windowSeconds = Math.round(this.config.windowMs / 1000);
40
- return {
41
- isSuspicious: true,
42
- type: 'rate_limit',
43
- severity: 'warning',
44
- reason: `Request threshold exceeded (${status.count}/${this.config.maxRequests} in ${windowSeconds}s)`,
45
- count: status.count,
46
- maxRequests: this.config.maxRequests,
47
- remaining: status.remaining,
48
- resetTime: status.resetTime,
49
- timestamp: new Date(now).toISOString()
50
- };
51
- }
52
-
53
- return {
54
- isSuspicious: false,
55
- type: 'normal',
56
- severity: 'info',
57
- reason: null,
58
- count: status.count,
59
- maxRequests: this.config.maxRequests,
60
- remaining: status.remaining,
61
- resetTime: status.resetTime,
62
- timestamp: new Date(now).toISOString()
63
- };
64
- }
65
-
66
- /**
67
- * Inspects current status without incrementing counters.
68
- *
69
- * @param {string} ip
70
- * @param {number} [now=Date.now()]
71
- * @returns {Object}
72
- */
73
- inspect(ip, now = Date.now()) {
74
- const status = this.rateLimiter.get(ip, now);
75
-
76
- return {
77
- isSuspicious: status.isExceeded,
78
- count: status.count,
79
- maxRequests: this.config.maxRequests,
80
- remaining: status.remaining,
81
- resetTime: status.resetTime
82
- };
83
- }
84
- }
85
-
86
- module.exports = {
87
- Detector
88
- };
package/src/index.js DELETED
@@ -1,97 +0,0 @@
1
- 'use strict';
2
-
3
- const { createMiddleware } = require('./middleware');
4
- const { resolveConfig, DEFAULT_CONFIG } = require('./config');
5
- const { RateLimiter } = require('./rate-limiter');
6
- const { Detector } = require('./detector');
7
- const { BlockManager } = require('./block-manager');
8
- const { Logger } = require('./logger');
9
-
10
- /**
11
- * Main factory function for initializing Z-Shield.
12
- *
13
- * Usage in Express:
14
- * const { createShield } = require('z-shield');
15
- * const shield = createShield({ mode: 'protect', maxRequests: 50 });
16
- * app.use(shield.middleware);
17
- *
18
- * Programmatic Block API:
19
- * shield.block('198.51.100.1', '10m', 'Manual administrative block');
20
- * shield.unblock('198.51.100.1');
21
- * shield.isBlocked('198.51.100.1');
22
- *
23
- * @param {Object} [options={}] - Custom configuration options
24
- * @returns {Object} Z-Shield instance with middleware, managers, logger, and programmatic methods
25
- */
26
- function createShield(options = {}) {
27
- const config = resolveConfig(options);
28
- const rateLimiter = new RateLimiter(config);
29
- const detector = new Detector({ rateLimiter, config });
30
- const blockManager = new BlockManager(config);
31
- const logger = new Logger(config);
32
-
33
- const middleware = createMiddleware(config, {
34
- rateLimiter,
35
- detector,
36
- blockManager,
37
- logger
38
- });
39
-
40
- return {
41
- middleware,
42
- config,
43
- rateLimiter,
44
- detector,
45
- blockManager,
46
- logger,
47
-
48
- // Programmatic Block API with security event logging
49
- block: (ip, duration, reason) => {
50
- const result = blockManager.block(ip, duration, reason);
51
- logger.log({
52
- appName: config.appName,
53
- type: 'manual_block',
54
- severity: 'info',
55
- ip,
56
- method: 'INTERNAL',
57
- route: '/admin/block',
58
- reason: result.reason,
59
- action: 'blocked'
60
- });
61
- return result;
62
- },
63
-
64
- unblock: (ip) => {
65
- const result = blockManager.unblock(ip);
66
- if (result) {
67
- logger.log({
68
- appName: config.appName,
69
- type: 'manual_unblock',
70
- severity: 'info',
71
- ip,
72
- method: 'INTERNAL',
73
- route: '/admin/unblock',
74
- reason: 'Manual unblock requested',
75
- action: 'unblocked'
76
- });
77
- }
78
- return result;
79
- },
80
-
81
- isBlocked: (ip) => blockManager.isBlocked(ip),
82
- getBlockInfo: (ip) => blockManager.getBlockInfo(ip),
83
- listBlocks: () => blockManager.listBlocks(),
84
- flush: () => logger.flush()
85
- };
86
- }
87
-
88
- module.exports = {
89
- createShield,
90
- createMiddleware,
91
- resolveConfig,
92
- DEFAULT_CONFIG,
93
- RateLimiter,
94
- Detector,
95
- BlockManager,
96
- Logger
97
- };
package/src/logger.js DELETED
@@ -1,279 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
-
6
- /**
7
- * Strips ANSI terminal escape codes from untrusted strings to prevent terminal injection.
8
- * @param {string} str
9
- * @returns {string}
10
- */
11
- function stripAnsi(str) {
12
- // Matches standard ANSI color and control escape sequences
13
- return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
14
- }
15
-
16
- /**
17
- * Sanitizes an untrusted string to prevent Log Injection (CWE-117).
18
- * - Strips ANSI escapes
19
- * - Strips control characters (newlines \r, \n, null bytes, backspaces, tabs)
20
- * - Truncates to maxLength to prevent disk space exhaustion
21
- *
22
- * @param {*} val
23
- * @param {number} [maxLength=250]
24
- * @returns {string}
25
- */
26
- function sanitizeString(val, maxLength = 250) {
27
- if (val === null || val === undefined) {
28
- return '';
29
- }
30
- let str = String(val);
31
- str = stripAnsi(str);
32
- // Replace control characters (ASCII 0-31, 127) with a clean space
33
- str = str.replace(/[\x00-\x1f\x7f]/g, ' ').trim();
34
- // Collapse consecutive spaces
35
- str = str.replace(/\s+/g, ' ');
36
- return str.slice(0, maxLength);
37
- }
38
-
39
- /**
40
- * Sanitizes HTTP route paths.
41
- * @param {*} route
42
- * @param {number} [maxLength=500]
43
- * @returns {string}
44
- */
45
- function sanitizeRoute(route, maxLength = 500) {
46
- if (!route || typeof route !== 'string') {
47
- return '/';
48
- }
49
- let clean = stripAnsi(route);
50
- // Strip query parameters and URL fragments to prevent credential leakage (CWE-532)
51
- clean = clean.split('?')[0].split('#')[0];
52
- // Remove dangerous control characters and newlines
53
- clean = clean.replace(/[\x00-\x1f\x7f]/g, '');
54
- if (!clean.startsWith('/')) {
55
- clean = '/' + clean;
56
- }
57
- return clean.slice(0, maxLength);
58
- }
59
-
60
- /**
61
- * Sanitizes HTTP methods.
62
- * @param {*} method
63
- * @returns {string}
64
- */
65
- function sanitizeMethod(method) {
66
- if (!method || typeof method !== 'string') {
67
- return 'GET';
68
- }
69
- const clean = method.toUpperCase().replace(/[^A-Z]/g, '');
70
- return clean.slice(0, 10) || 'GET';
71
- }
72
-
73
- /**
74
- * Logger - Safe, Persistent Local JSON Lines Security Event Logger
75
- *
76
- * Design Features:
77
- * - JSON Lines (.jsonl): Every event is on exactly one line ending in \n.
78
- * - Non-Blocking & Asynchronous: Uses node:fs/promises without blocking the event loop.
79
- * - Strict Sanitization: Eliminates Log Injection (CWE-117) by stripping newlines and ANSI escapes.
80
- * - Privacy-Preserving: Never logs passwords, cookies, authorization headers, or request bodies (CWE-532).
81
- * - Bounded Storage: Automatic log rotation and retention limits prevent disk space exhaustion.
82
- * - Resilient: Filesystem errors (ENOSPC, EACCES) are safely caught and will not crash the host app.
83
- * - Sequential Queue: Guarantees concurrency safety so simultaneous writes never corrupt files.
84
- *
85
- * Hosting Notice:
86
- * Logs are stored in local persistent storage. If deployed to ephemeral platforms
87
- * (e.g. basic Heroku dynos, AWS Lambda, or serverless containers without mounted volumes),
88
- * local log files may be lost upon container restart or redeployment.
89
- */
90
- class Logger {
91
- /**
92
- * @param {Object} [config={}] - Resolved Z-Shield configuration
93
- */
94
- constructor(config = {}) {
95
- this.appName = config.appName || 'default-app';
96
- this.logDir = path.resolve(config.logDir || path.join(process.cwd(), '.z-shield-logs'));
97
- this.logFilename = config.logFilename || 'events.jsonl';
98
- this.filePath = path.join(this.logDir, this.logFilename);
99
- this.maxLogSizeBytes = config.maxLogSizeBytes || 5 * 1024 * 1024; // 5MB
100
- this.maxLogFiles = typeof config.maxLogFiles === 'number' ? config.maxLogFiles : 5;
101
- this.onLogError = typeof config.onLogError === 'function' ? config.onLogError : null;
102
-
103
- this._writeQueue = Promise.resolve();
104
- this._dirEnsured = false;
105
- }
106
-
107
- /**
108
- * Formats and sanitizes a security event into a validated schema.
109
- * Deliberately omits request bodies, authorization headers, cookies, and tokens.
110
- *
111
- * @param {Object} rawEvent
112
- * @returns {Object} Validated security event
113
- */
114
- formatEvent(rawEvent = {}) {
115
- const now = rawEvent.timestamp ? new Date(rawEvent.timestamp) : new Date();
116
- const timestamp = isNaN(now.getTime()) ? new Date().toISOString() : now.toISOString();
117
-
118
- const allowedSeverities = ['info', 'warning', 'critical'];
119
- const severity = allowedSeverities.includes(rawEvent.severity) ? rawEvent.severity : 'warning';
120
-
121
- const allowedActions = ['blocked', 'warned', 'allowed', 'unblocked'];
122
- const action = allowedActions.includes(rawEvent.action) ? rawEvent.action : 'warned';
123
-
124
- return {
125
- timestamp,
126
- appName: sanitizeString(rawEvent.appName || this.appName, 50),
127
- type: sanitizeString(rawEvent.type || 'security_event', 30),
128
- severity,
129
- ip: sanitizeString(rawEvent.ip || 'unknown', 45),
130
- method: sanitizeMethod(rawEvent.method),
131
- route: sanitizeRoute(rawEvent.route),
132
- reason: sanitizeString(rawEvent.reason || 'Unspecified security event', 300),
133
- action
134
- };
135
- }
136
-
137
- /**
138
- * Safely logs a security event asynchronously.
139
- * Resolves to true if write succeeded, or false if it failed.
140
- *
141
- * @param {Object} rawEvent
142
- * @returns {Promise<boolean>}
143
- */
144
- log(rawEvent) {
145
- return new Promise((resolve) => {
146
- this._writeQueue = this._writeQueue
147
- .then(() => this._writeEvent(rawEvent))
148
- .then(() => resolve(true))
149
- .catch((err) => {
150
- this._handleError(err, rawEvent);
151
- resolve(false);
152
- });
153
- });
154
- }
155
-
156
- /**
157
- * Waits for all currently queued asynchronous log writes to complete.
158
- * Useful for deterministic testing, graceful shutdowns, and process exit handlers.
159
- *
160
- * @returns {Promise<void>}
161
- */
162
- async flush() {
163
- await this._writeQueue;
164
- }
165
-
166
- /**
167
- * Internal write operation executed within the sequential queue.
168
- *
169
- * @private
170
- * @param {Object} rawEvent
171
- */
172
- async _writeEvent(rawEvent) {
173
- const event = this.formatEvent(rawEvent);
174
- await this._ensureDir();
175
- await this._rotateIfNeeded();
176
-
177
- const line = JSON.stringify(event) + '\n';
178
- await fs.promises.appendFile(this.filePath, line, { encoding: 'utf8', flag: 'a' });
179
- }
180
-
181
- /**
182
- * Ensures the target log directory exists.
183
- * @private
184
- */
185
- async _ensureDir() {
186
- if (!this._dirEnsured) {
187
- await fs.promises.mkdir(this.logDir, { recursive: true });
188
- this._dirEnsured = true;
189
- }
190
- }
191
-
192
- /**
193
- * Checks file size and rotates log files if size exceeds maxLogSizeBytes.
194
- * @private
195
- */
196
- async _rotateIfNeeded() {
197
- try {
198
- const stats = await fs.promises.stat(this.filePath);
199
- if (stats.size < this.maxLogSizeBytes) {
200
- return;
201
- }
202
- } catch (err) {
203
- if (err.code === 'ENOENT') {
204
- return; // File does not exist yet
205
- }
206
- throw err;
207
- }
208
-
209
- const { name, ext } = path.parse(this.logFilename);
210
-
211
- // If maxLogFiles is 0, simply remove the current file
212
- if (this.maxLogFiles === 0) {
213
- try {
214
- await fs.promises.unlink(this.filePath);
215
- } catch (err) {
216
- if (err.code !== 'ENOENT') throw err;
217
- }
218
- return;
219
- }
220
-
221
- // Shift existing rotated files:
222
- // e.g. events.5.jsonl deleted (retention limit)
223
- // events.4.jsonl -> events.5.jsonl
224
- // events.1.jsonl -> events.2.jsonl
225
- for (let i = this.maxLogFiles; i >= 1; i--) {
226
- const currentRotated = path.join(this.logDir, `${name}.${i}${ext}`);
227
- if (i === this.maxLogFiles) {
228
- try {
229
- await fs.promises.unlink(currentRotated);
230
- } catch (err) {
231
- if (err.code !== 'ENOENT') throw err;
232
- }
233
- } else {
234
- const nextRotated = path.join(this.logDir, `${name}.${i + 1}${ext}`);
235
- try {
236
- await fs.promises.rename(currentRotated, nextRotated);
237
- } catch (err) {
238
- if (err.code !== 'ENOENT') throw err;
239
- }
240
- }
241
- }
242
-
243
- // Move current events.jsonl to events.1.jsonl
244
- const firstRotated = path.join(this.logDir, `${name}.1${ext}`);
245
- try {
246
- await fs.promises.rename(this.filePath, firstRotated);
247
- } catch (err) {
248
- if (err.code !== 'ENOENT') throw err;
249
- }
250
- }
251
-
252
- /**
253
- * Safely handles write and filesystem failures without crashing the application.
254
- *
255
- * @private
256
- * @param {Error} err
257
- * @param {Object} rawEvent
258
- */
259
- _handleError(err, rawEvent) {
260
- if (this.onLogError) {
261
- try {
262
- this.onLogError(err, rawEvent);
263
- } catch {
264
- // Prevent errors in the user's callback from crashing the app
265
- }
266
- } else {
267
- // Print clear, non-fatal warning to stderr
268
- console.error(`[Z-Shield Logger Warning]: Failed to write security event log: ${err.message}`);
269
- }
270
- }
271
- }
272
-
273
- module.exports = {
274
- Logger,
275
- sanitizeString,
276
- sanitizeRoute,
277
- sanitizeMethod,
278
- stripAnsi
279
- };