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.
Files changed (47) hide show
  1. package/.claude/settings.local.json +6 -1
  2. package/.eslintrc.json +50 -0
  3. package/.github/workflows/ci.yml +317 -0
  4. package/.prettierrc +10 -0
  5. package/CHANGES.md +296 -0
  6. package/DEPLOYMENT.md +956 -0
  7. package/FIXES_APPLIED.md +378 -0
  8. package/FORTUNE_500_UPGRADE.md +863 -0
  9. package/MasterAction.js +10 -263
  10. package/MasterControl.js +226 -43
  11. package/MasterRequest.js +42 -1
  12. package/MasterRouter.js +42 -37
  13. package/PERFORMANCE_SECURITY_AUDIT.md +677 -0
  14. package/README.md +602 -71
  15. package/SENIOR_ENGINEER_AUDIT.md +2477 -0
  16. package/VERIFICATION_CHECKLIST.md +726 -0
  17. package/error/README.md +2452 -0
  18. package/monitoring/HealthCheck.js +347 -0
  19. package/monitoring/PrometheusExporter.js +416 -0
  20. package/monitoring/README.md +3112 -0
  21. package/package.json +64 -11
  22. package/security/MasterValidator.js +140 -10
  23. package/security/README.md +1805 -0
  24. package/security/adapters/RedisCSRFStore.js +428 -0
  25. package/security/adapters/RedisRateLimiter.js +462 -0
  26. package/security/adapters/RedisSessionStore.js +476 -0
  27. package/MasterCors.js.tmp +0 -0
  28. package/MasterHtml.js +0 -649
  29. package/MasterPipeline.js.tmp +0 -0
  30. package/MasterRequest.js.tmp +0 -0
  31. package/MasterRouter.js.tmp +0 -0
  32. package/MasterSocket.js.tmp +0 -0
  33. package/MasterTemp.js.tmp +0 -0
  34. package/MasterTemplate.js +0 -230
  35. package/MasterTimeout.js.tmp +0 -0
  36. package/TemplateOverwrite.js +0 -41
  37. package/TemplateOverwrite.js.tmp +0 -0
  38. package/error/ErrorBoundary.js +0 -353
  39. package/error/HydrationMismatch.js +0 -265
  40. package/error/MasterError.js +0 -240
  41. package/error/MasterError.js.tmp +0 -0
  42. package/error/MasterErrorRenderer.js +0 -536
  43. package/error/MasterErrorRenderer.js.tmp +0 -0
  44. package/error/SSRErrorHandler.js +0 -273
  45. package/ssr/hydration-client.js +0 -93
  46. package/ssr/runtime-ssr.cjs +0 -553
  47. 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
+ };