axiom-purifylog 1.0.0

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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/index.js +261 -0
  4. package/package.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alessandro Ghilardi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # axiom-purifylog
2
+
3
+ A privacy-first Node.js logger with automatic sensitive data redaction, file rotation, and a built-in HTTP log viewer.
4
+
5
+ ## Features
6
+
7
+ - 🔒 **Automatic Sensitive Data Redaction**: Recursively masks emails, UUIDs, long tokens, and blacklisted keys (passwords, API keys, IPs, etc.) in both strings and deep JSON objects.
8
+ - 🔄 **Built-in File Rotation**: Automatically rotates and suffixes log files when they reach the maximum size limit (defaults to 10MB).
9
+ - 📂 **Structured Level Directory Separation**: Automatically organizes logs into structured directories based on their level (`/logs/info`, `/logs/error`, etc.).
10
+ - 🌐 **On-the-fly HTTP Log Viewer**: Expose a lightweight endpoint to safely browse and read your raw log files directly from a browser or via API.
11
+ - 🔌 **Express Middleware**: Out-of-the-box HTTP request logging middleware with automatic `/status` health-check filtering.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install axiom-purifylog
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```javascript
22
+ const logger = require('axiom-purifylog');
23
+
24
+ // Simple logs
25
+ logger.info('Application started successfully');
26
+ logger.warn('Database connection retrying...');
27
+ logger.error('Failed to load resource');
28
+ ```
29
+
30
+ ## Feature Deep Dive
31
+
32
+ ### 1. Automatic Sensitive Data Masking (Redaction)
33
+
34
+ axiom-purifylog automatically intercepts strings and objects to sanitize sensitive information before printing to stdout or writing to disk.
35
+
36
+ ```javascript
37
+ logger.info({
38
+ message: "User login attempt",
39
+ email: "john.doe@example.com", // Will be redacted
40
+ metadata: {
41
+ ip: "192.168.1.1", // Will be redacted
42
+ sessionToken: "a-very-long-token-identifier-that-should-not-leak", // Will be redacted
43
+ nested: {
44
+ password: "super-secret-password-123", // Will be redacted
45
+ address: "123 Main St, Anytown, USA", // Will be redacted
46
+ active: true // Kept intact
47
+ }
48
+ }
49
+ });
50
+ ```
51
+
52
+ **Output in Console / Log File:**
53
+
54
+ ```json
55
+ {
56
+ "message": "User login attempt",
57
+ "email": "DATA-REDACTED",
58
+ "metadata": {
59
+ "ip": "DATA-REDACTED",
60
+ "sessionToken": "DATA-REDACTED",
61
+ "nested": {
62
+ "password": "DATA-REDACTED",
63
+ "active": true
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ **Default Redacted Keys & Patterns:**
70
+
71
+ - **Regex Patterns**: Emails, UUIDs, and strings longer than 30 characters matching token formats.
72
+ - **Sensitive Keys**: `to`, `email`, `username`, `subject`, `code`, `otp`, `password`, `pwd`, `secret`, `token`, `auth`, `uuid`, `phone`, `vat`, `device`, `address`, `key`, `host`, `database`, `port`, `env`, `node_version`, `ip`.
73
+
74
+ ### 2. Configuration (.setup())
75
+
76
+ Configure the logger instance at the entry point of your application.
77
+
78
+ ```javascript
79
+ logger.setup({
80
+ logDir: 'custom-logs-folder', // Relative or absolute path (default: './logs')
81
+ maxFileSize: 5 * 1024 * 1024, // Max file size in bytes before rotation (default: 10MB)
82
+ minLevel: 'debug' // Minimum level to log: 'error' | 'warn' | 'info' | 'debug'
83
+ });
84
+ ```
85
+
86
+ **Note**: You can also set the minimum logging level globally using the `LOG_LEVEL` environment variable.
87
+
88
+ ### 3. Express Middleware
89
+
90
+ Easily log incoming HTTP requests. The middleware automatically measures response duration and skips noisy `/status` endpoints.
91
+
92
+ ```javascript
93
+ const express = require('express');
94
+ const logger = require('axiom-purifylog');
95
+
96
+ const app = express();
97
+
98
+ app.use(logger.getRequestMiddleware());
99
+
100
+ app.get('/users', (req, res) => {
101
+ res.json({ ok: true });
102
+ });
103
+ ```
104
+
105
+ ### 4. Built-in HTTP Log Viewer
106
+
107
+ Expose an administrative endpoint to browse directories and read log files directly over HTTP. Path traversal attacks are prevented out-of-the-box by strict directory boundaries.
108
+
109
+ ```javascript
110
+ const http = require('http');
111
+ const logger = require('axiom-purifylog');
112
+
113
+ http.createServer((req, res) => {
114
+ // Route logs requests to the logger handler
115
+ if (req.url.startsWith('/admin/logs')) {
116
+ // Strip the custom prefix so the logger maps to the correct internal directories
117
+ req.url = req.url.replace('/admin/logs', '') || '/';
118
+ return logger.handleRequest(req, res);
119
+ }
120
+
121
+ res.writeHead(404);
122
+ res.end('Not Found');
123
+ }).listen(3000, () => {
124
+ console.log('Admin log viewer available at http://localhost:3000/admin/logs');
125
+ });
126
+ ```
127
+
128
+ **API / Viewer Behavior:**
129
+
130
+ - `GET /`: Returns JSON list of available log level directories: `{"levels":["info","error"]}`
131
+ - `GET /info`: Returns JSON list of files in that folder: `{"category":"info","files":["info-2026-06-04.txt"]}`
132
+ - `GET /info/info-2026-06-04.txt`: Streams the raw log file back as `text/plain`.
133
+
134
+ ## Log File Structure & Rotation
135
+
136
+ Logs are saved on disk using the following hierarchy:
137
+
138
+ ```
139
+ logs/
140
+ ├── info/
141
+ │ ├── info-2026-06-04.txt
142
+ │ └── info-2026-06-04-1.txt (Rotated file)
143
+ ├── error/
144
+ │ └── error-2026-06-04.txt
145
+ └── debug/
146
+ ```
147
+
148
+ Each line in the log file is a structured JSON string, making it easy to parse with log aggregators:
149
+
150
+ ```json
151
+ {"timestamp":"2026-06-04 18:45:00","timestamp_us":"06/04/2026, 18:45:00","level":"INFO","message":"Server listening on port 3000"}
152
+ ```
153
+
154
+ ## License
155
+
156
+ This project is licensed under the MIT License.
package/index.js ADDED
@@ -0,0 +1,261 @@
1
+ const fs = require('node:fs').promises;
2
+ const fsSync = require('node:fs');
3
+ const path = require('node:path');
4
+
5
+ /**
6
+ * A logger class that provides structured logging with automatic file rotation,
7
+ * sensitive data redaction, and Express middleware support.
8
+ * @class
9
+ */
10
+ class Logger {
11
+ /**
12
+ * Creates a new Logger instance with default configuration.
13
+ * @constructor
14
+ */
15
+ constructor() {
16
+ this.baseLogDir = path.join(process.cwd(), 'logs');
17
+ this.maxFileSize = 10 * 1024 * 1024;
18
+
19
+ this.priorities = { 'error': 0, 'warn': 1, 'info': 2, 'debug': 3 };
20
+ this.minLevel = process.env.LOG_LEVEL || 'info';
21
+
22
+ this.emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
23
+ this.uuidRegex = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
24
+ this.longTokenRegex = /\b[a-z0-9-_]{30,}/gi;
25
+
26
+ this.sensitiveKeys = [
27
+ 'to', 'email', 'username', 'subject', 'code', 'otp',
28
+ 'password', 'pwd', 'secret', 'token', 'auth', 'uuid',
29
+ 'phone', 'vat', 'device', 'address', 'key', 'host', 'database',
30
+ 'port', 'env', 'node_version', 'ip'
31
+ ];
32
+ }
33
+
34
+ /**
35
+ * Configures the logger with custom settings.
36
+ * @param {Object} config - Configuration options
37
+ * @param {string} [config.logDir] - Directory for log files (relative or absolute path)
38
+ * @param {number} [config.maxFileSize] - Maximum file size in bytes before rotation (default: 10MB)
39
+ * @param {string} [config.minLevel] - Minimum log level ('error', 'warn', 'info', 'debug')
40
+ * @returns {Logger} The logger instance for method chaining
41
+ */
42
+ setup(config = {}) {
43
+ if (config.logDir) {
44
+ this.baseLogDir = path.isAbsolute(config.logDir) ? config.logDir : path.join(process.cwd(), config.logDir);
45
+ }
46
+ if (config.maxFileSize) this.maxFileSize = config.maxFileSize;
47
+ if (config.minLevel && this.priorities[config.minLevel] !== undefined) {
48
+ this.minLevel = config.minLevel;
49
+ }
50
+ return this;
51
+ }
52
+
53
+ /**
54
+ * Gets the current timestamp in ISO format without milliseconds.
55
+ * @private
56
+ * @returns {string} Formatted timestamp (YYYY-MM-DD HH:MM:SS)
57
+ */
58
+ _getTimestamp() {
59
+ return new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
60
+ }
61
+
62
+ /**
63
+ * Gets the current timestamp in US locale format.
64
+ * @private
65
+ * @returns {string} Formatted US timestamp (MM/DD/YYYY, HH:MM:SS)
66
+ */
67
+ _getUSTimestamp() {
68
+ return new Date().toLocaleString('en-US', {
69
+ year: 'numeric', month: '2-digit', day: '2-digit',
70
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Gets the current date as a tag for log file naming.
76
+ * @private
77
+ * @returns {string} Date tag in YYYY-MM-DD format
78
+ */
79
+ _getDateTag() {
80
+ const d = new Date();
81
+ return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`;
82
+ }
83
+
84
+ /**
85
+ * Redacts sensitive information from log messages.
86
+ * Masks emails, UUIDs, long tokens, and sensitive key values.
87
+ * @private
88
+ * @param {string|Object} message - The message to redact
89
+ * @returns {string|Object} The redacted message
90
+ */
91
+ _redact(message) {
92
+ const maskValue = (val) => {
93
+ if (typeof val === 'string') {
94
+ let m = val.replace(this.emailRegex, 'DATA-REDACTED').replace(this.uuidRegex, 'DATA-REDACTED');
95
+ if (m !== 'DATA-REDACTED' && m.length > 30) m = m.replace(this.longTokenRegex, 'DATA-REDACTED');
96
+ return m;
97
+ }
98
+ return val;
99
+ };
100
+
101
+ const recursiveRedact = (obj) => {
102
+ if (Array.isArray(obj)) return obj.map(item => recursiveRedact(item));
103
+ if (obj !== null && typeof obj === 'object') {
104
+ const newObj = {};
105
+ for (const key in obj) {
106
+ const lowKey = key.toLowerCase();
107
+ newObj[key] = this.sensitiveKeys.some(k => lowKey.includes(k)) ? 'DATA-REDACTED' : recursiveRedact(obj[key]);
108
+ }
109
+ return newObj;
110
+ }
111
+ return maskValue(obj);
112
+ };
113
+
114
+ try {
115
+ if (typeof message === 'object' && message !== null) {
116
+ return recursiveRedact(message);
117
+ }
118
+ return maskValue(String(message));
119
+ } catch (e) { return '[REDACTION_ERROR]'; }
120
+ }
121
+
122
+ /**
123
+ * Internal logging method that handles message output and file writing.
124
+ * @private
125
+ * @async
126
+ * @param {string} level - The log level ('error', 'warn', 'info', 'debug')
127
+ * @param {string|Object} message - The message to log
128
+ */
129
+ async _log(level, message) {
130
+ if (this.priorities[level] > this.priorities[this.minLevel]) return;
131
+
132
+ const tsStd = this._getTimestamp();
133
+ const tsUS = this._getUSTimestamp();
134
+ const redacted = this._redact(message);
135
+
136
+ const logObject = {
137
+ timestamp: tsStd,
138
+ timestamp_us: tsUS,
139
+ level: level.toUpperCase(),
140
+ message: redacted
141
+ };
142
+ const logEntry = JSON.stringify(logObject) + '\n';
143
+
144
+ const colors = { info: '\x1b[32m', error: '\x1b[31m', warn: '\x1b[33m', debug: '\x1b[36m', reset: '\x1b[0m' };
145
+ const color = colors[level] || colors.reset;
146
+ process.stdout.write(`${color}[${level.toUpperCase()}]${colors.reset} [${tsUS}] ${typeof redacted === 'object' ? JSON.stringify(redacted, null, 2) : redacted}\n`);
147
+
148
+ try {
149
+ const dirPath = path.join(this.baseLogDir, level);
150
+ if (!fsSync.existsSync(dirPath)) await fs.mkdir(dirPath, { recursive: true });
151
+
152
+ const dateTag = this._getDateTag();
153
+ let filePath = path.join(dirPath, `${level}-${dateTag}.txt`);
154
+
155
+ try {
156
+ const stats = await fs.stat(filePath);
157
+ if (stats.size >= this.maxFileSize) {
158
+ let suffix = 1;
159
+ while (fsSync.existsSync(path.join(dirPath, `${level}-${dateTag}-${suffix}.txt`))) suffix++;
160
+ filePath = path.join(dirPath, `${level}-${dateTag}-${suffix}.txt`);
161
+ }
162
+ } catch (e) { }
163
+
164
+ await fs.appendFile(filePath, logEntry, 'utf8');
165
+ } catch (err) {
166
+ process.stderr.write(`Logger Critical Error: ${err.message}\n`);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Logs an info level message.
172
+ * @param {string|Object} msg - The message to log
173
+ */
174
+ info(msg) { this._log('info', msg); }
175
+
176
+ /**
177
+ * Logs an error level message.
178
+ * @param {string|Object} msg - The message to log
179
+ */
180
+ error(msg) { this._log('error', msg); }
181
+
182
+ /**
183
+ * Logs a warning level message.
184
+ * @param {string|Object} msg - The message to log
185
+ */
186
+ warn(msg) { this._log('warn', msg); }
187
+
188
+ /**
189
+ * Logs a debug level message.
190
+ * @param {string|Object} msg - The message to log
191
+ */
192
+ debug(msg) { this._log('debug', msg); }
193
+
194
+ /**
195
+ * Returns Express middleware for logging HTTP requests.
196
+ * Skips logging for /status endpoint.
197
+ * @returns {Function} Express middleware function
198
+ */
199
+ getRequestMiddleware() {
200
+ return (req, res, next) => {
201
+ if (req.url === '/status' || req.path === '/status') {
202
+ return next();
203
+ }
204
+
205
+ const start = Date.now();
206
+ res.on('finish', () => {
207
+ const duration = Date.now() - start;
208
+ this.info({
209
+ event: 'HTTP_REQUEST',
210
+ method: req.method,
211
+ url: req.url,
212
+ status: res.statusCode,
213
+ duration: `${duration}ms`
214
+ });
215
+ });
216
+ next();
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Handles HTTP requests for viewing log files and directories.
222
+ * Provides a simple API to browse and read log files.
223
+ * @param {Object} req - HTTP request object
224
+ * @param {Object} res - HTTP response object
225
+ */
226
+ handleRequest(req, res) {
227
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
228
+ const parts = url.pathname.split('/').filter(Boolean);
229
+ res.setHeader('Content-Type', 'application/json');
230
+
231
+ const safePath = path.resolve(this.baseLogDir, ...parts);
232
+ if (!safePath.startsWith(path.resolve(this.baseLogDir))) {
233
+ res.statusCode = 403;
234
+ return res.end(JSON.stringify({ error: 'Access Denied' }));
235
+ }
236
+
237
+ if (parts.length === 0) {
238
+ const levels = fsSync.existsSync(this.baseLogDir) ? fsSync.readdirSync(this.baseLogDir) : [];
239
+ return res.end(JSON.stringify({ levels }));
240
+ }
241
+
242
+ if (!fsSync.existsSync(safePath)) {
243
+ res.statusCode = 404;
244
+ return res.end(JSON.stringify({ error: 'Not Found' }));
245
+ }
246
+
247
+ const stats = fsSync.statSync(safePath);
248
+ if (stats.isDirectory()) {
249
+ return res.end(JSON.stringify({ category: parts[parts.length - 1], files: fsSync.readdirSync(safePath) }));
250
+ }
251
+
252
+ if (stats.isFile()) {
253
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
254
+ const readStream = fsSync.createReadStream(safePath);
255
+ readStream.on('error', () => { res.statusCode = 500; res.end("Read Error"); });
256
+ return readStream.pipe(res);
257
+ }
258
+ }
259
+ }
260
+
261
+ module.exports = new Logger();
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "axiom-purifylog",
3
+ "version": "1.0.0",
4
+ "description": "A privacy-first Node.js logger with automatic sensitive data redaction, file rotation, and a built-in HTTP log viewer.",
5
+ "main": "index.js",
6
+ "keywords": [
7
+ "logger",
8
+ "logging",
9
+ "privacy",
10
+ "security",
11
+ "redaction",
12
+ "sensitive-data",
13
+ "file-rotation",
14
+ "log-viewer",
15
+ "http",
16
+ "nodejs",
17
+ "privacy-first",
18
+ "data-protection",
19
+ "gdpr",
20
+ "pii",
21
+ "personal-data"
22
+ ],
23
+ "author": "Alessandro Ghilardi",
24
+ "license": "MIT",
25
+ "engines": {
26
+ "node": ">=14"
27
+ },
28
+ "files": [
29
+ "index.js",
30
+ "README.md",
31
+ "LICENSE"
32
+ ]
33
+ }