mastercontroller 1.3.9 → 1.3.12
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/.claude/settings.local.json +6 -1
- package/.eslintrc.json +50 -0
- package/.github/workflows/ci.yml +317 -0
- package/.prettierrc +10 -0
- package/CHANGES.md +296 -0
- package/DEPLOYMENT.md +956 -0
- package/FIXES_APPLIED.md +378 -0
- package/FORTUNE_500_UPGRADE.md +863 -0
- package/MasterAction.js +10 -263
- package/MasterControl.js +226 -43
- package/MasterRequest.js +42 -1
- package/MasterRouter.js +42 -37
- package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
- package/README.md +602 -71
- package/SENIOR_ENGINEER_AUDIT.md +2477 -0
- package/VERIFICATION_CHECKLIST.md +726 -0
- package/error/README.md +2452 -0
- package/monitoring/HealthCheck.js +347 -0
- package/monitoring/PrometheusExporter.js +416 -0
- package/monitoring/README.md +3112 -0
- package/package.json +64 -11
- package/security/MasterValidator.js +140 -10
- package/security/README.md +1805 -0
- package/security/adapters/RedisCSRFStore.js +428 -0
- package/security/adapters/RedisRateLimiter.js +462 -0
- package/security/adapters/RedisSessionStore.js +476 -0
- package/MasterCors.js.tmp +0 -0
- package/MasterHtml.js +0 -649
- package/MasterPipeline.js.tmp +0 -0
- package/MasterRequest.js.tmp +0 -0
- package/MasterRouter.js.tmp +0 -0
- package/MasterSocket.js.tmp +0 -0
- package/MasterTemp.js.tmp +0 -0
- package/MasterTemplate.js +0 -230
- package/MasterTimeout.js.tmp +0 -0
- package/TemplateOverwrite.js +0 -41
- package/TemplateOverwrite.js.tmp +0 -0
- package/error/ErrorBoundary.js +0 -353
- package/error/HydrationMismatch.js +0 -265
- package/error/MasterError.js +0 -240
- package/error/MasterError.js.tmp +0 -0
- package/error/MasterErrorRenderer.js +0 -536
- package/error/MasterErrorRenderer.js.tmp +0 -0
- package/error/SSRErrorHandler.js +0 -273
- package/ssr/hydration-client.js +0 -93
- package/ssr/runtime-ssr.cjs +0 -553
- package/ssr/ssr-shims.js +0 -73
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RedisCSRFStore - Distributed CSRF token storage for horizontal scaling
|
|
3
|
+
* Version: 1.0.0
|
|
4
|
+
*
|
|
5
|
+
* Stores CSRF tokens in Redis to enable token validation across multiple
|
|
6
|
+
* MasterController instances in load-balanced deployments.
|
|
7
|
+
*
|
|
8
|
+
* Installation:
|
|
9
|
+
* npm install ioredis --save
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* const Redis = require('ioredis');
|
|
14
|
+
* const { RedisCSRFStore } = require('./security/adapters/RedisCSRFStore');
|
|
15
|
+
*
|
|
16
|
+
* const redis = new Redis({ host: 'localhost', port: 6379 });
|
|
17
|
+
*
|
|
18
|
+
* const csrfStore = new RedisCSRFStore(redis, {
|
|
19
|
+
* ttl: 3600 // 1 hour token lifetime
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Use with MasterController CSRF middleware
|
|
23
|
+
* master.csrf.setStore(csrfStore);
|
|
24
|
+
*
|
|
25
|
+
* Features:
|
|
26
|
+
* - Distributed CSRF token validation
|
|
27
|
+
* - Automatic token expiration
|
|
28
|
+
* - Per-session token storage
|
|
29
|
+
* - Token rotation support
|
|
30
|
+
* - Graceful degradation
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const crypto = require('crypto');
|
|
34
|
+
const { logger } = require('../../error/MasterErrorLogger');
|
|
35
|
+
|
|
36
|
+
class RedisCSRFStore {
|
|
37
|
+
constructor(redisClient, options = {}) {
|
|
38
|
+
if (!redisClient) {
|
|
39
|
+
throw new Error('RedisCSRFStore requires a Redis client (ioredis)');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.redis = redisClient;
|
|
43
|
+
this.options = {
|
|
44
|
+
prefix: options.prefix || 'mastercontroller:csrf:',
|
|
45
|
+
ttl: options.ttl || 3600, // 1 hour default
|
|
46
|
+
tokenLength: options.tokenLength || 32,
|
|
47
|
+
...options
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
logger.info({
|
|
51
|
+
code: 'MC_CSRF_REDIS_INIT',
|
|
52
|
+
message: 'Redis CSRF store initialized',
|
|
53
|
+
prefix: this.options.prefix,
|
|
54
|
+
ttl: this.options.ttl
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate Redis key for CSRF token
|
|
60
|
+
*/
|
|
61
|
+
_getKey(sessionId) {
|
|
62
|
+
return `${this.options.prefix}${sessionId}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate Redis key for token-to-session mapping
|
|
67
|
+
*/
|
|
68
|
+
_getTokenKey(token) {
|
|
69
|
+
return `${this.options.prefix}token:${token}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate cryptographically secure token
|
|
74
|
+
*/
|
|
75
|
+
_generateToken() {
|
|
76
|
+
return crypto.randomBytes(this.options.tokenLength).toString('hex');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create new CSRF token for session
|
|
81
|
+
*/
|
|
82
|
+
async create(sessionId) {
|
|
83
|
+
try {
|
|
84
|
+
const token = this._generateToken();
|
|
85
|
+
const key = this._getKey(sessionId);
|
|
86
|
+
const tokenKey = this._getTokenKey(token);
|
|
87
|
+
|
|
88
|
+
// Store session -> token mapping
|
|
89
|
+
await this.redis.setex(key, this.options.ttl, token);
|
|
90
|
+
|
|
91
|
+
// Store token -> session mapping (for validation)
|
|
92
|
+
await this.redis.setex(tokenKey, this.options.ttl, sessionId);
|
|
93
|
+
|
|
94
|
+
logger.debug({
|
|
95
|
+
code: 'MC_CSRF_TOKEN_CREATED',
|
|
96
|
+
message: 'CSRF token created',
|
|
97
|
+
sessionId: sessionId
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return token;
|
|
101
|
+
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error({
|
|
104
|
+
code: 'MC_CSRF_CREATE_ERROR',
|
|
105
|
+
message: 'Failed to create CSRF token',
|
|
106
|
+
sessionId: sessionId,
|
|
107
|
+
error: error.message
|
|
108
|
+
});
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get CSRF token for session (or create if doesn't exist)
|
|
115
|
+
*/
|
|
116
|
+
async get(sessionId) {
|
|
117
|
+
try {
|
|
118
|
+
const key = this._getKey(sessionId);
|
|
119
|
+
const token = await this.redis.get(key);
|
|
120
|
+
|
|
121
|
+
if (token) {
|
|
122
|
+
logger.debug({
|
|
123
|
+
code: 'MC_CSRF_TOKEN_RETRIEVED',
|
|
124
|
+
message: 'CSRF token retrieved',
|
|
125
|
+
sessionId: sessionId
|
|
126
|
+
});
|
|
127
|
+
return token;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// No token exists, create new one
|
|
131
|
+
return await this.create(sessionId);
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.error({
|
|
135
|
+
code: 'MC_CSRF_GET_ERROR',
|
|
136
|
+
message: 'Failed to get CSRF token',
|
|
137
|
+
sessionId: sessionId,
|
|
138
|
+
error: error.message
|
|
139
|
+
});
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate CSRF token
|
|
146
|
+
*/
|
|
147
|
+
async validate(sessionId, token) {
|
|
148
|
+
try {
|
|
149
|
+
if (!token || !sessionId) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const tokenKey = this._getTokenKey(token);
|
|
154
|
+
const storedSessionId = await this.redis.get(tokenKey);
|
|
155
|
+
|
|
156
|
+
if (!storedSessionId) {
|
|
157
|
+
logger.warn({
|
|
158
|
+
code: 'MC_CSRF_TOKEN_NOT_FOUND',
|
|
159
|
+
message: 'CSRF token not found or expired',
|
|
160
|
+
sessionId: sessionId
|
|
161
|
+
});
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Verify token belongs to this session
|
|
166
|
+
if (storedSessionId !== sessionId) {
|
|
167
|
+
logger.error({
|
|
168
|
+
code: 'MC_CSRF_TOKEN_MISMATCH',
|
|
169
|
+
message: 'CSRF token session mismatch - possible attack',
|
|
170
|
+
sessionId: sessionId,
|
|
171
|
+
tokenSession: storedSessionId
|
|
172
|
+
});
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger.debug({
|
|
177
|
+
code: 'MC_CSRF_TOKEN_VALID',
|
|
178
|
+
message: 'CSRF token validated',
|
|
179
|
+
sessionId: sessionId
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.error({
|
|
186
|
+
code: 'MC_CSRF_VALIDATE_ERROR',
|
|
187
|
+
message: 'Failed to validate CSRF token',
|
|
188
|
+
sessionId: sessionId,
|
|
189
|
+
error: error.message
|
|
190
|
+
});
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Invalidate CSRF token
|
|
197
|
+
*/
|
|
198
|
+
async invalidate(sessionId) {
|
|
199
|
+
try {
|
|
200
|
+
const key = this._getKey(sessionId);
|
|
201
|
+
const token = await this.redis.get(key);
|
|
202
|
+
|
|
203
|
+
if (token) {
|
|
204
|
+
const tokenKey = this._getTokenKey(token);
|
|
205
|
+
await this.redis.del(tokenKey);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this.redis.del(key);
|
|
209
|
+
|
|
210
|
+
logger.debug({
|
|
211
|
+
code: 'MC_CSRF_TOKEN_INVALIDATED',
|
|
212
|
+
message: 'CSRF token invalidated',
|
|
213
|
+
sessionId: sessionId
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return true;
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
logger.error({
|
|
220
|
+
code: 'MC_CSRF_INVALIDATE_ERROR',
|
|
221
|
+
message: 'Failed to invalidate CSRF token',
|
|
222
|
+
sessionId: sessionId,
|
|
223
|
+
error: error.message
|
|
224
|
+
});
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Rotate CSRF token (invalidate old, create new)
|
|
231
|
+
* Used after sensitive operations or periodically for security
|
|
232
|
+
*/
|
|
233
|
+
async rotate(sessionId) {
|
|
234
|
+
try {
|
|
235
|
+
// Invalidate old token
|
|
236
|
+
await this.invalidate(sessionId);
|
|
237
|
+
|
|
238
|
+
// Create new token
|
|
239
|
+
const newToken = await this.create(sessionId);
|
|
240
|
+
|
|
241
|
+
logger.info({
|
|
242
|
+
code: 'MC_CSRF_TOKEN_ROTATED',
|
|
243
|
+
message: 'CSRF token rotated',
|
|
244
|
+
sessionId: sessionId
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return newToken;
|
|
248
|
+
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error({
|
|
251
|
+
code: 'MC_CSRF_ROTATE_ERROR',
|
|
252
|
+
message: 'Failed to rotate CSRF token',
|
|
253
|
+
sessionId: sessionId,
|
|
254
|
+
error: error.message
|
|
255
|
+
});
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Refresh token TTL without changing the token
|
|
262
|
+
*/
|
|
263
|
+
async refresh(sessionId) {
|
|
264
|
+
try {
|
|
265
|
+
const key = this._getKey(sessionId);
|
|
266
|
+
const token = await this.redis.get(key);
|
|
267
|
+
|
|
268
|
+
if (!token) {
|
|
269
|
+
// No token exists, create new one
|
|
270
|
+
return await this.create(sessionId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Refresh both mappings
|
|
274
|
+
await this.redis.expire(key, this.options.ttl);
|
|
275
|
+
|
|
276
|
+
const tokenKey = this._getTokenKey(token);
|
|
277
|
+
await this.redis.expire(tokenKey, this.options.ttl);
|
|
278
|
+
|
|
279
|
+
logger.debug({
|
|
280
|
+
code: 'MC_CSRF_TOKEN_REFRESHED',
|
|
281
|
+
message: 'CSRF token TTL refreshed',
|
|
282
|
+
sessionId: sessionId
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return token;
|
|
286
|
+
|
|
287
|
+
} catch (error) {
|
|
288
|
+
logger.error({
|
|
289
|
+
code: 'MC_CSRF_REFRESH_ERROR',
|
|
290
|
+
message: 'Failed to refresh CSRF token',
|
|
291
|
+
sessionId: sessionId,
|
|
292
|
+
error: error.message
|
|
293
|
+
});
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Middleware factory for CSRF protection
|
|
300
|
+
*/
|
|
301
|
+
middleware(options = {}) {
|
|
302
|
+
const self = this;
|
|
303
|
+
const {
|
|
304
|
+
ignoreMethods = ['GET', 'HEAD', 'OPTIONS'],
|
|
305
|
+
tokenHeader = 'x-csrf-token',
|
|
306
|
+
tokenField = '_csrf',
|
|
307
|
+
errorMessage = 'Invalid CSRF token'
|
|
308
|
+
} = options;
|
|
309
|
+
|
|
310
|
+
return async (ctx, next) => {
|
|
311
|
+
try {
|
|
312
|
+
// Get session ID from context
|
|
313
|
+
const sessionId = ctx.session?.id || ctx.sessionId;
|
|
314
|
+
|
|
315
|
+
if (!sessionId) {
|
|
316
|
+
logger.warn({
|
|
317
|
+
code: 'MC_CSRF_NO_SESSION',
|
|
318
|
+
message: 'CSRF check skipped - no session ID',
|
|
319
|
+
path: ctx.request.url
|
|
320
|
+
});
|
|
321
|
+
return await next();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Skip CSRF check for safe methods
|
|
325
|
+
const method = ctx.request.method.toUpperCase();
|
|
326
|
+
if (ignoreMethods.includes(method)) {
|
|
327
|
+
// Ensure token exists for this session
|
|
328
|
+
await self.get(sessionId);
|
|
329
|
+
return await next();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Get token from request (header or body)
|
|
333
|
+
const token = ctx.request.headers[tokenHeader.toLowerCase()]
|
|
334
|
+
|| ctx.body?.[tokenField]
|
|
335
|
+
|| ctx.query?.[tokenField];
|
|
336
|
+
|
|
337
|
+
if (!token) {
|
|
338
|
+
logger.warn({
|
|
339
|
+
code: 'MC_CSRF_TOKEN_MISSING',
|
|
340
|
+
message: 'CSRF token missing in request',
|
|
341
|
+
sessionId: sessionId,
|
|
342
|
+
path: ctx.request.url
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
ctx.response.statusCode = 403;
|
|
346
|
+
ctx.response.setHeader('Content-Type', 'application/json');
|
|
347
|
+
ctx.response.end(JSON.stringify({
|
|
348
|
+
error: 'Forbidden',
|
|
349
|
+
message: 'CSRF token required'
|
|
350
|
+
}));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Validate token
|
|
355
|
+
const valid = await self.validate(sessionId, token);
|
|
356
|
+
|
|
357
|
+
if (!valid) {
|
|
358
|
+
logger.error({
|
|
359
|
+
code: 'MC_CSRF_VALIDATION_FAILED',
|
|
360
|
+
message: 'CSRF token validation failed',
|
|
361
|
+
sessionId: sessionId,
|
|
362
|
+
path: ctx.request.url,
|
|
363
|
+
ip: ctx.request.connection.remoteAddress
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
ctx.response.statusCode = 403;
|
|
367
|
+
ctx.response.setHeader('Content-Type', 'application/json');
|
|
368
|
+
ctx.response.end(JSON.stringify({
|
|
369
|
+
error: 'Forbidden',
|
|
370
|
+
message: errorMessage
|
|
371
|
+
}));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Token valid, continue pipeline
|
|
376
|
+
await next();
|
|
377
|
+
|
|
378
|
+
} catch (error) {
|
|
379
|
+
logger.error({
|
|
380
|
+
code: 'MC_CSRF_MIDDLEWARE_ERROR',
|
|
381
|
+
message: 'CSRF middleware error',
|
|
382
|
+
error: error.message
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// On error, deny for security (fail closed)
|
|
386
|
+
ctx.response.statusCode = 500;
|
|
387
|
+
ctx.response.end('Internal Server Error');
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get CSRF token for use in templates/frontend
|
|
394
|
+
*/
|
|
395
|
+
async getTokenForTemplate(sessionId) {
|
|
396
|
+
return await this.get(sessionId);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Cleanup expired tokens (maintenance task)
|
|
401
|
+
* Note: Redis automatically expires keys, but this can be used for manual cleanup
|
|
402
|
+
*/
|
|
403
|
+
async cleanup() {
|
|
404
|
+
try {
|
|
405
|
+
// Redis handles expiration automatically with TTL
|
|
406
|
+
// This method is here for compatibility/manual cleanup if needed
|
|
407
|
+
|
|
408
|
+
logger.info({
|
|
409
|
+
code: 'MC_CSRF_CLEANUP',
|
|
410
|
+
message: 'CSRF token cleanup completed (Redis auto-expires)'
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return true;
|
|
414
|
+
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.error({
|
|
417
|
+
code: 'MC_CSRF_CLEANUP_ERROR',
|
|
418
|
+
message: 'Failed to cleanup CSRF tokens',
|
|
419
|
+
error: error.message
|
|
420
|
+
});
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = {
|
|
427
|
+
RedisCSRFStore
|
|
428
|
+
};
|