mastercontroller 1.3.35 → 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/MasterControl.js +4 -4
- package/MasterCors.js +1 -1
- package/MasterPipeline.js +2 -2
- package/MasterTimeout.js +2 -2
- package/error/MasterErrorLogger.js +79 -35
- package/error/MasterErrorMiddleware.js +20 -15
- package/monitoring/HealthCheck.js +4 -2
- package/monitoring/PrometheusExporter.js +1 -1
- package/package.json +1 -1
- package/security/CSPConfig.js +3 -2
- package/security/SecurityEnforcement.js +1 -1
- package/security/SecurityMiddleware.js +18 -13
- package/security/SessionSecurity.js +21 -12
- package/security/adapters/RedisCSRFStore.js +5 -3
- package/security/adapters/RedisRateLimiter.js +2 -2
package/MasterControl.js
CHANGED
|
@@ -902,7 +902,7 @@ class MasterControl {
|
|
|
902
902
|
return; // Terminal - don't call next()
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
-
await next(); // Not static, continue pipeline
|
|
905
|
+
if (typeof next === 'function') await next(); // Not static, continue pipeline
|
|
906
906
|
});
|
|
907
907
|
|
|
908
908
|
// 2. Timeout Tracking (optional - disabled by default until init)
|
|
@@ -924,7 +924,7 @@ class MasterControl {
|
|
|
924
924
|
ctx.params.formData = params.formData;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
await next();
|
|
927
|
+
if (typeof next === 'function') await next();
|
|
928
928
|
});
|
|
929
929
|
|
|
930
930
|
// 4. Load Scoped Services (per request - always needed)
|
|
@@ -938,7 +938,7 @@ class MasterControl {
|
|
|
938
938
|
const className = $that._scopedList[key];
|
|
939
939
|
$that.requestList[key] = new className();
|
|
940
940
|
}
|
|
941
|
-
await next();
|
|
941
|
+
if (typeof next === 'function') await next();
|
|
942
942
|
});
|
|
943
943
|
|
|
944
944
|
// 4. HSTS Header (if enabled for HTTPS)
|
|
@@ -954,7 +954,7 @@ class MasterControl {
|
|
|
954
954
|
}
|
|
955
955
|
ctx.response.setHeader('Strict-Transport-Security', hstsValue);
|
|
956
956
|
}
|
|
957
|
-
await next();
|
|
957
|
+
if (typeof next === 'function') await next();
|
|
958
958
|
});
|
|
959
959
|
|
|
960
960
|
// 5. Routing and Error Handler are registered in start() so that user
|
package/MasterCors.js
CHANGED
package/MasterPipeline.js
CHANGED
|
@@ -116,12 +116,12 @@ class MasterPipeline {
|
|
|
116
116
|
// Execute branch pipeline
|
|
117
117
|
await branch.execute(ctx);
|
|
118
118
|
// Stop if response already sent (e.g., auth rejection)
|
|
119
|
-
if (!ctx.response.headersSent && !ctx.response.writableEnded) {
|
|
119
|
+
if (!ctx.response.headersSent && !ctx.response.writableEnded && typeof next === 'function') {
|
|
120
120
|
await next();
|
|
121
121
|
}
|
|
122
122
|
} else {
|
|
123
123
|
// Skip branch, continue main pipeline
|
|
124
|
-
await next();
|
|
124
|
+
if (typeof next === 'function') await next();
|
|
125
125
|
}
|
|
126
126
|
};
|
|
127
127
|
|
package/MasterTimeout.js
CHANGED
|
@@ -483,7 +483,7 @@ class MasterTimeout {
|
|
|
483
483
|
|
|
484
484
|
return async (ctx, next) => {
|
|
485
485
|
if (!$that.enabled) {
|
|
486
|
-
await next();
|
|
486
|
+
if (typeof next === 'function') await next();
|
|
487
487
|
return;
|
|
488
488
|
}
|
|
489
489
|
|
|
@@ -493,7 +493,7 @@ class MasterTimeout {
|
|
|
493
493
|
requestId = $that.startTracking(ctx);
|
|
494
494
|
ctx.requestId = requestId;
|
|
495
495
|
|
|
496
|
-
await next();
|
|
496
|
+
if (typeof next === 'function') await next();
|
|
497
497
|
} catch (err) {
|
|
498
498
|
// Stop tracking on error (with error handling)
|
|
499
499
|
if (requestId) {
|
|
@@ -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({
|
|
@@ -55,7 +55,8 @@ class HealthCheck {
|
|
|
55
55
|
|
|
56
56
|
// Only handle health check endpoint
|
|
57
57
|
if (requestPath !== self.options.endpoint) {
|
|
58
|
-
return await next();
|
|
58
|
+
if (typeof next === 'function') return await next();
|
|
59
|
+
return;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// Log health check request
|
|
@@ -226,7 +227,8 @@ class HealthCheck {
|
|
|
226
227
|
const requestPath = req.url.split('?')[0];
|
|
227
228
|
|
|
228
229
|
if (requestPath !== self.options.endpoint) {
|
|
229
|
-
return next();
|
|
230
|
+
if (typeof next === 'function') return next();
|
|
231
|
+
return;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
try {
|
package/package.json
CHANGED
package/security/CSPConfig.js
CHANGED
|
@@ -95,7 +95,8 @@ class CSPConfig {
|
|
|
95
95
|
middleware() {
|
|
96
96
|
return (req, res, next) => {
|
|
97
97
|
if (!this.enabled) {
|
|
98
|
-
|
|
98
|
+
if (typeof next === 'function') next();
|
|
99
|
+
return;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
// Generate nonce for this request if needed
|
|
@@ -111,7 +112,7 @@ class CSPConfig {
|
|
|
111
112
|
const headerName = this.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
|
|
112
113
|
res.setHeader(headerName, headerValue);
|
|
113
114
|
|
|
114
|
-
next();
|
|
115
|
+
if (typeof next === 'function') next();
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -74,7 +74,8 @@ class SecurityMiddleware {
|
|
|
74
74
|
*/
|
|
75
75
|
securityHeadersMiddleware(req, res, next) {
|
|
76
76
|
if (!this.headersEnabled) {
|
|
77
|
-
|
|
77
|
+
if (typeof next === 'function') next();
|
|
78
|
+
return;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
// Apply standard security headers
|
|
@@ -96,7 +97,7 @@ class SecurityMiddleware {
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
next();
|
|
100
|
+
if (typeof next === 'function') next();
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
/**
|
|
@@ -104,7 +105,8 @@ class SecurityMiddleware {
|
|
|
104
105
|
*/
|
|
105
106
|
corsMiddleware(req, res, next) {
|
|
106
107
|
if (!this.corsEnabled) {
|
|
107
|
-
|
|
108
|
+
if (typeof next === 'function') next();
|
|
109
|
+
return;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
const origin = req.headers.origin;
|
|
@@ -125,7 +127,7 @@ class SecurityMiddleware {
|
|
|
125
127
|
return;
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
next();
|
|
130
|
+
if (typeof next === 'function') next();
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
/**
|
|
@@ -133,7 +135,8 @@ class SecurityMiddleware {
|
|
|
133
135
|
*/
|
|
134
136
|
rateLimitMiddleware(req, res, next) {
|
|
135
137
|
if (!this.rateLimitEnabled) {
|
|
136
|
-
|
|
138
|
+
if (typeof next === 'function') next();
|
|
139
|
+
return;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
const identifier = this._getClientIdentifier(req);
|
|
@@ -209,7 +212,7 @@ class SecurityMiddleware {
|
|
|
209
212
|
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
210
213
|
res.setHeader('X-RateLimit-Reset', new Date(resetTime).toISOString());
|
|
211
214
|
|
|
212
|
-
next();
|
|
215
|
+
if (typeof next === 'function') next();
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/**
|
|
@@ -217,13 +220,15 @@ class SecurityMiddleware {
|
|
|
217
220
|
*/
|
|
218
221
|
csrfMiddleware(req, res, next) {
|
|
219
222
|
if (!this.csrfEnabled) {
|
|
220
|
-
|
|
223
|
+
if (typeof next === 'function') next();
|
|
224
|
+
return;
|
|
221
225
|
}
|
|
222
226
|
|
|
223
227
|
// Skip CSRF for safe methods
|
|
224
228
|
const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
|
|
225
229
|
if (safeMethods.includes(req.method)) {
|
|
226
|
-
|
|
230
|
+
if (typeof next === 'function') next();
|
|
231
|
+
return;
|
|
227
232
|
}
|
|
228
233
|
|
|
229
234
|
// Get CSRF token from request
|
|
@@ -291,7 +296,7 @@ class SecurityMiddleware {
|
|
|
291
296
|
}
|
|
292
297
|
|
|
293
298
|
// Token valid, continue
|
|
294
|
-
next();
|
|
299
|
+
if (typeof next === 'function') next();
|
|
295
300
|
}
|
|
296
301
|
|
|
297
302
|
/**
|
|
@@ -489,7 +494,7 @@ function pipelineSecurityHeaders(options = {}) {
|
|
|
489
494
|
instance.securityHeadersMiddleware(ctx.request, ctx.response, oldNext);
|
|
490
495
|
|
|
491
496
|
// Continue pipeline if next was called
|
|
492
|
-
if (nextCalled) {
|
|
497
|
+
if (nextCalled && typeof next === 'function') {
|
|
493
498
|
await next();
|
|
494
499
|
}
|
|
495
500
|
};
|
|
@@ -504,7 +509,7 @@ function pipelineCors(options = {}) {
|
|
|
504
509
|
instance.corsMiddleware(ctx.request, ctx.response, oldNext);
|
|
505
510
|
|
|
506
511
|
// CORS might terminate for OPTIONS - check if response ended
|
|
507
|
-
if (!ctx.response.writableEnded && nextCalled) {
|
|
512
|
+
if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
|
|
508
513
|
await next();
|
|
509
514
|
}
|
|
510
515
|
};
|
|
@@ -519,7 +524,7 @@ function pipelineRateLimit(options = {}) {
|
|
|
519
524
|
instance.rateLimitMiddleware(ctx.request, ctx.response, oldNext);
|
|
520
525
|
|
|
521
526
|
// Rate limit might terminate - check if response ended
|
|
522
|
-
if (!ctx.response.writableEnded && nextCalled) {
|
|
527
|
+
if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
|
|
523
528
|
await next();
|
|
524
529
|
}
|
|
525
530
|
};
|
|
@@ -534,7 +539,7 @@ function pipelineCsrf(options = {}) {
|
|
|
534
539
|
instance.csrfMiddleware(ctx.request, ctx.response, oldNext);
|
|
535
540
|
|
|
536
541
|
// CSRF might terminate - check if response ended
|
|
537
|
-
if (!ctx.response.writableEnded && nextCalled) {
|
|
542
|
+
if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
|
|
538
543
|
await next();
|
|
539
544
|
}
|
|
540
545
|
};
|
|
@@ -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,23 +81,25 @@ 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
|
-
next
|
|
100
|
+
if (typeof next === 'function') {
|
|
101
|
+
await next();
|
|
102
|
+
}
|
|
94
103
|
};
|
|
95
104
|
}
|
|
96
105
|
|
|
@@ -318,7 +318,8 @@ class RedisCSRFStore {
|
|
|
318
318
|
message: 'CSRF check skipped - no session ID',
|
|
319
319
|
path: ctx.request.url
|
|
320
320
|
});
|
|
321
|
-
return await next();
|
|
321
|
+
if (typeof next === 'function') return await next();
|
|
322
|
+
return;
|
|
322
323
|
}
|
|
323
324
|
|
|
324
325
|
// Skip CSRF check for safe methods
|
|
@@ -326,7 +327,8 @@ class RedisCSRFStore {
|
|
|
326
327
|
if (ignoreMethods.includes(method)) {
|
|
327
328
|
// Ensure token exists for this session
|
|
328
329
|
await self.get(sessionId);
|
|
329
|
-
return await next();
|
|
330
|
+
if (typeof next === 'function') return await next();
|
|
331
|
+
return;
|
|
330
332
|
}
|
|
331
333
|
|
|
332
334
|
// Get token from request (header or body)
|
|
@@ -373,7 +375,7 @@ class RedisCSRFStore {
|
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
// Token valid, continue pipeline
|
|
376
|
-
await next();
|
|
378
|
+
if (typeof next === 'function') await next();
|
|
377
379
|
|
|
378
380
|
} catch (error) {
|
|
379
381
|
logger.error({
|
|
@@ -441,7 +441,7 @@ class RedisRateLimiter {
|
|
|
441
441
|
}
|
|
442
442
|
|
|
443
443
|
// Request allowed, continue pipeline
|
|
444
|
-
await next();
|
|
444
|
+
if (typeof next === 'function') await next();
|
|
445
445
|
|
|
446
446
|
} catch (error) {
|
|
447
447
|
logger.error({
|
|
@@ -451,7 +451,7 @@ class RedisRateLimiter {
|
|
|
451
451
|
});
|
|
452
452
|
|
|
453
453
|
// On error, allow request (fail open)
|
|
454
|
-
await next();
|
|
454
|
+
if (typeof next === 'function') await next();
|
|
455
455
|
}
|
|
456
456
|
};
|
|
457
457
|
}
|