mastercontroller 1.3.36 ā 1.3.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/error/MasterErrorLogger.js +79 -35
- package/error/MasterErrorMiddleware.js +20 -15
- package/package.json +1 -1
- package/security/SessionSecurity.js +19 -12
|
@@ -32,12 +32,14 @@ class MasterErrorLogger {
|
|
|
32
32
|
file: options.file || null,
|
|
33
33
|
sampleRate: options.sampleRate || 1.0, // Log 100% by default
|
|
34
34
|
maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB
|
|
35
|
+
dedupeWindowMs: options.dedupeWindowMs || 5000, // Suppress duplicate errors within 5s
|
|
35
36
|
...options
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
this.backends = [];
|
|
39
40
|
this.errorCount = 0;
|
|
40
41
|
this.sessionId = this._generateSessionId();
|
|
42
|
+
this._recentErrors = new Map(); // code -> { count, firstSeen, lastSeen }
|
|
41
43
|
|
|
42
44
|
// Setup default backends
|
|
43
45
|
if (this.options.console) {
|
|
@@ -76,18 +78,47 @@ class MasterErrorLogger {
|
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
// Deduplicate repeated errors within the window
|
|
82
|
+
const code = data.code || 'UNKNOWN';
|
|
83
|
+
if (level >= LOG_LEVELS.ERROR) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const recent = this._recentErrors.get(code);
|
|
86
|
+
if (recent && (now - recent.firstSeen) < this.options.dedupeWindowMs) {
|
|
87
|
+
recent.count++;
|
|
88
|
+
recent.lastSeen = now;
|
|
89
|
+
return; // Suppress duplicate
|
|
90
|
+
}
|
|
91
|
+
// Flush summary of previous burst if there was one
|
|
92
|
+
if (recent && recent.count > 1) {
|
|
93
|
+
const summary = this._formatLogEntry({
|
|
94
|
+
code: code,
|
|
95
|
+
message: `Suppressed ${recent.count - 1} duplicate entries of ${code} (${recent.lastSeen - recent.firstSeen}ms window)`,
|
|
96
|
+
level: LOG_LEVELS.WARN
|
|
97
|
+
}, LOG_LEVELS.WARN);
|
|
98
|
+
this._dispatch(summary);
|
|
99
|
+
}
|
|
100
|
+
this._recentErrors.set(code, { count: 1, firstSeen: now, lastSeen: now });
|
|
101
|
+
}
|
|
102
|
+
|
|
79
103
|
const entry = this._formatLogEntry(data, level);
|
|
104
|
+
this._dispatch(entry);
|
|
105
|
+
this.errorCount++;
|
|
106
|
+
}
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Dispatch entry to all backends
|
|
110
|
+
*/
|
|
111
|
+
_dispatch(entry) {
|
|
82
112
|
this.backends.forEach(backend => {
|
|
83
113
|
try {
|
|
84
114
|
backend(entry);
|
|
85
115
|
} catch (error) {
|
|
86
|
-
console
|
|
116
|
+
// Avoid console methods that can trigger EPIPE recursion
|
|
117
|
+
if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED') {
|
|
118
|
+
try { process.stderr.write(`[MasterErrorLogger] Backend failed: ${error.message}\n`); } catch (_) {}
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
});
|
|
89
|
-
|
|
90
|
-
this.errorCount++;
|
|
91
122
|
}
|
|
92
123
|
|
|
93
124
|
/**
|
|
@@ -117,28 +148,39 @@ class MasterErrorLogger {
|
|
|
117
148
|
* Format log entry with metadata
|
|
118
149
|
*/
|
|
119
150
|
_formatLogEntry(data, level) {
|
|
120
|
-
|
|
151
|
+
const entry = {
|
|
121
152
|
timestamp: new Date().toISOString(),
|
|
122
153
|
sessionId: this.sessionId,
|
|
123
154
|
level: LOG_LEVEL_NAMES[level],
|
|
124
155
|
code: data.code || 'UNKNOWN',
|
|
125
|
-
message: data.message || 'No message provided'
|
|
126
|
-
component: data.component || null,
|
|
127
|
-
file: data.file || null,
|
|
128
|
-
line: data.line || null,
|
|
129
|
-
route: data.route || null,
|
|
130
|
-
context: data.context || {},
|
|
131
|
-
stack: data.stack || null,
|
|
132
|
-
originalError: data.originalError ? {
|
|
133
|
-
message: data.originalError.message,
|
|
134
|
-
stack: data.originalError.stack
|
|
135
|
-
} : null,
|
|
136
|
-
environment: process.env.NODE_ENV || 'development',
|
|
137
|
-
nodeVersion: process.version,
|
|
138
|
-
platform: process.platform,
|
|
139
|
-
memory: process.memoryUsage(),
|
|
140
|
-
uptime: process.uptime()
|
|
156
|
+
message: data.message || 'No message provided'
|
|
141
157
|
};
|
|
158
|
+
|
|
159
|
+
// Only include optional fields when they have values
|
|
160
|
+
if (data.component) entry.component = data.component;
|
|
161
|
+
if (data.file) entry.file = data.file;
|
|
162
|
+
if (data.line) entry.line = data.line;
|
|
163
|
+
if (data.route) entry.route = data.route;
|
|
164
|
+
if (data.context && Object.keys(data.context).length > 0) entry.context = data.context;
|
|
165
|
+
|
|
166
|
+
// Include stack once ā prefer originalError.stack to avoid duplication
|
|
167
|
+
if (data.originalError) {
|
|
168
|
+
entry.stack = data.originalError.stack || data.stack || null;
|
|
169
|
+
} else if (data.stack) {
|
|
170
|
+
entry.stack = data.stack;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
entry.environment = process.env.NODE_ENV || 'development';
|
|
174
|
+
|
|
175
|
+
// Only include memory/system info on ERROR and FATAL
|
|
176
|
+
if (level >= LOG_LEVELS.ERROR) {
|
|
177
|
+
entry.memory = process.memoryUsage();
|
|
178
|
+
entry.nodeVersion = process.version;
|
|
179
|
+
entry.platform = process.platform;
|
|
180
|
+
entry.uptime = process.uptime();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return entry;
|
|
142
184
|
}
|
|
143
185
|
|
|
144
186
|
/**
|
|
@@ -156,24 +198,26 @@ class MasterErrorLogger {
|
|
|
156
198
|
const color = levelColors[entry.level] || '';
|
|
157
199
|
const reset = '\x1b[0m';
|
|
158
200
|
|
|
159
|
-
|
|
160
|
-
|
|
201
|
+
// Use process.stdout/stderr.write directly to avoid EPIPE recursion
|
|
202
|
+
// through console.log -> broken pipe -> uncaughtException -> logger -> console.log
|
|
203
|
+
const stream = (entry.level === 'DEBUG' || entry.level === 'INFO') ? process.stdout : process.stderr;
|
|
161
204
|
|
|
162
|
-
|
|
163
|
-
`${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}
|
|
164
|
-
entry.message
|
|
165
|
-
);
|
|
205
|
+
try {
|
|
206
|
+
stream.write(`${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}: ${entry.message}\n`);
|
|
166
207
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
if (entry.component) {
|
|
209
|
+
stream.write(` Component: ${entry.component}\n`);
|
|
210
|
+
}
|
|
170
211
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
212
|
+
if (entry.file) {
|
|
213
|
+
stream.write(` File: ${entry.file}${entry.line ? `:${entry.line}` : ''}\n`);
|
|
214
|
+
}
|
|
174
215
|
|
|
175
|
-
|
|
176
|
-
|
|
216
|
+
if (entry.stack && process.env.NODE_ENV !== 'production') {
|
|
217
|
+
stream.write(` Stack: ${entry.stack}\n`);
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
// If the stream is broken (EPIPE), silently drop ā do not recurse
|
|
177
221
|
}
|
|
178
222
|
}
|
|
179
223
|
|
|
@@ -172,7 +172,14 @@ function extractUserCodeContext(stack) {
|
|
|
172
172
|
function setupGlobalErrorHandlers() {
|
|
173
173
|
// Handle uncaught exceptions
|
|
174
174
|
process.on('uncaughtException', (error) => {
|
|
175
|
-
|
|
175
|
+
// EPIPE/stream errors from logging itself ā do not recurse
|
|
176
|
+
if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
|
|
177
|
+
try { process.stderr.write(`[MasterController] Stream error suppressed: ${error.code}\n`); } catch (_) {}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Use stderr.write instead of console.error to avoid EPIPE recursion
|
|
182
|
+
try { process.stderr.write(`[MasterController] Uncaught Exception: ${error.message}\n`); } catch (_) {}
|
|
176
183
|
|
|
177
184
|
// Extract context from stack trace
|
|
178
185
|
const context = extractUserCodeContext(error.stack);
|
|
@@ -181,34 +188,33 @@ function setupGlobalErrorHandlers() {
|
|
|
181
188
|
let enhancedMessage = `Uncaught exception: ${error.message}`;
|
|
182
189
|
|
|
183
190
|
if (context && context.triggeringFile) {
|
|
184
|
-
enhancedMessage += `\n\
|
|
191
|
+
enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
if (context && context.userFiles.length > 0) {
|
|
188
|
-
enhancedMessage += `\n\
|
|
195
|
+
enhancedMessage += `\n\nYour Code Involved:`;
|
|
189
196
|
context.userFiles.forEach((file, i) => {
|
|
190
|
-
if (i < 3) {
|
|
197
|
+
if (i < 3) {
|
|
191
198
|
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
192
199
|
}
|
|
193
200
|
});
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
if (context && context.frameworkFiles.length > 0) {
|
|
197
|
-
enhancedMessage += `\n\
|
|
204
|
+
enhancedMessage += `\n\nFramework Files Involved:`;
|
|
198
205
|
context.frameworkFiles.forEach((file, i) => {
|
|
199
|
-
if (i < 2) {
|
|
206
|
+
if (i < 2) {
|
|
200
207
|
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
201
208
|
}
|
|
202
209
|
});
|
|
203
210
|
}
|
|
204
211
|
|
|
205
|
-
|
|
212
|
+
try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
|
|
206
213
|
|
|
207
214
|
logger.fatal({
|
|
208
215
|
code: 'MC_ERR_UNCAUGHT_EXCEPTION',
|
|
209
216
|
message: enhancedMessage,
|
|
210
217
|
originalError: error,
|
|
211
|
-
stack: error.stack,
|
|
212
218
|
context: context
|
|
213
219
|
});
|
|
214
220
|
|
|
@@ -220,7 +226,7 @@ function setupGlobalErrorHandlers() {
|
|
|
220
226
|
|
|
221
227
|
// Handle unhandled promise rejections
|
|
222
228
|
process.on('unhandledRejection', (reason, promise) => {
|
|
223
|
-
|
|
229
|
+
try { process.stderr.write(`[MasterController] Unhandled Rejection: ${reason}\n`); } catch (_) {}
|
|
224
230
|
|
|
225
231
|
// Extract context from stack trace if available
|
|
226
232
|
const context = reason?.stack ? extractUserCodeContext(reason.stack) : null;
|
|
@@ -229,27 +235,26 @@ function setupGlobalErrorHandlers() {
|
|
|
229
235
|
let enhancedMessage = `Unhandled promise rejection: ${reason}`;
|
|
230
236
|
|
|
231
237
|
if (context && context.triggeringFile) {
|
|
232
|
-
enhancedMessage += `\n\
|
|
238
|
+
enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
if (context && context.userFiles.length > 0) {
|
|
236
|
-
enhancedMessage += `\n\
|
|
242
|
+
enhancedMessage += `\n\nYour Code Involved:`;
|
|
237
243
|
context.userFiles.forEach((file, i) => {
|
|
238
|
-
if (i < 3) {
|
|
244
|
+
if (i < 3) {
|
|
239
245
|
enhancedMessage += `\n ${i + 1}. ${file.location}`;
|
|
240
246
|
}
|
|
241
247
|
});
|
|
242
248
|
}
|
|
243
249
|
|
|
244
250
|
if (enhancedMessage !== `Unhandled promise rejection: ${reason}`) {
|
|
245
|
-
|
|
251
|
+
try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
logger.error({
|
|
249
255
|
code: 'MC_ERR_UNHANDLED_REJECTION',
|
|
250
256
|
message: enhancedMessage,
|
|
251
257
|
originalError: reason,
|
|
252
|
-
stack: reason?.stack,
|
|
253
258
|
context: context
|
|
254
259
|
});
|
|
255
260
|
});
|
|
@@ -257,7 +262,7 @@ function setupGlobalErrorHandlers() {
|
|
|
257
262
|
// Handle warnings
|
|
258
263
|
process.on('warning', (warning) => {
|
|
259
264
|
if (isDevelopment) {
|
|
260
|
-
|
|
265
|
+
try { process.stderr.write(`[MasterController] Warning: ${warning.message}\n`); } catch (_) {}
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
logger.warn({
|
package/package.json
CHANGED
|
@@ -36,18 +36,25 @@ class SessionSecurity {
|
|
|
36
36
|
* Session middleware
|
|
37
37
|
*/
|
|
38
38
|
middleware() {
|
|
39
|
-
|
|
39
|
+
const self = this;
|
|
40
|
+
|
|
41
|
+
// Return pipeline-compatible (ctx, next) middleware.
|
|
42
|
+
// MasterPipeline.execute() calls handler(ctx, next), not handler(req, res, next).
|
|
43
|
+
return async (ctx, next) => {
|
|
44
|
+
const req = ctx.request;
|
|
45
|
+
const res = ctx.response;
|
|
46
|
+
|
|
40
47
|
// Parse session from cookie
|
|
41
|
-
const sessionId =
|
|
48
|
+
const sessionId = self._parseCookie(req);
|
|
42
49
|
|
|
43
50
|
if (sessionId) {
|
|
44
51
|
// Load existing session
|
|
45
52
|
const session = sessionStore.get(sessionId);
|
|
46
53
|
|
|
47
|
-
if (session &&
|
|
54
|
+
if (session && self._isSessionValid(session, req)) {
|
|
48
55
|
// Check if session needs regeneration
|
|
49
|
-
if (
|
|
50
|
-
req.session = await
|
|
56
|
+
if (self._shouldRegenerate(session)) {
|
|
57
|
+
req.session = await self._regenerateSession(sessionId, session, res);
|
|
51
58
|
} else {
|
|
52
59
|
// Use existing session
|
|
53
60
|
req.session = session.data;
|
|
@@ -57,9 +64,9 @@ class SessionSecurity {
|
|
|
57
64
|
session.lastAccess = Date.now();
|
|
58
65
|
|
|
59
66
|
// Extend expiry if rolling
|
|
60
|
-
if (
|
|
61
|
-
session.expiry = Date.now() +
|
|
62
|
-
|
|
67
|
+
if (self.rolling) {
|
|
68
|
+
session.expiry = Date.now() + self.maxAge;
|
|
69
|
+
self._setCookie(res, sessionId);
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
} else {
|
|
@@ -74,24 +81,24 @@ class SessionSecurity {
|
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
// Create new session
|
|
77
|
-
req.session = await
|
|
84
|
+
req.session = await self._createSession(req, res);
|
|
78
85
|
}
|
|
79
86
|
} else {
|
|
80
87
|
// No session cookie, create new session
|
|
81
|
-
req.session = await
|
|
88
|
+
req.session = await self._createSession(req, res);
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
// Save session on response
|
|
85
92
|
if (typeof res?.end === 'function') {
|
|
86
93
|
const originalEnd = res.end;
|
|
87
94
|
res.end = (...args) => {
|
|
88
|
-
|
|
95
|
+
self._saveSession(req);
|
|
89
96
|
originalEnd.apply(res, args);
|
|
90
97
|
};
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
if (typeof next === 'function') {
|
|
94
|
-
next();
|
|
101
|
+
await next();
|
|
95
102
|
}
|
|
96
103
|
};
|
|
97
104
|
}
|