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
package/MasterAction.js CHANGED
@@ -3,16 +3,13 @@
3
3
 
4
4
  var fileserver = require('fs');
5
5
  var toolClass = require('./MasterTools');
6
- var tempClass = require('./MasterTemplate');
7
- // Templating helpers
8
- var temp = new tempClass();
9
6
  var tools = new toolClass();
7
+ // View templating removed - handled by view engine (e.g., MasterView)
10
8
 
11
9
  // Node utils
12
10
  var path = require('path');
13
11
 
14
- // Vanilla Web Components SSR runtime (LinkeDOM) - executes connectedCallback() and upgrades
15
- const compileWebComponentsHTML = require('./ssr/runtime-ssr.cjs');
12
+ // SSR runtime removed - handled by view engine
16
13
 
17
14
  // Enhanced error handling
18
15
  const { handleTemplateError, sendErrorResponse } = require('./error/MasterBackendErrorHandler');
@@ -35,22 +32,7 @@ class MasterAction{
35
32
  return MasterAction.__masterCache;
36
33
  }
37
34
 
38
- getView(location, data){
39
- var actionUrl = MasterAction._master.root + location;
40
- const fileResult = safeReadFile(fileserver, actionUrl);
41
-
42
- if (!fileResult.success) {
43
- const error = handleTemplateError(fileResult.error.originalError, actionUrl, data);
44
- throw error;
45
- }
46
-
47
- try {
48
- return temp.htmlBuilder(fileResult.content, data);
49
- } catch (error) {
50
- const mcError = handleTemplateError(error, actionUrl, data);
51
- throw mcError;
52
- }
53
- }
35
+ // getView() removed - handled by view engine (register via master.useView())
54
36
 
55
37
 
56
38
  returnJson(data){
@@ -76,52 +58,7 @@ class MasterAction{
76
58
  }
77
59
  }
78
60
 
79
- // location starts from the view folder. Ex: partialViews/fileName.html
80
- returnPartialView(location, data){
81
- // SECURITY: Validate path to prevent traversal attacks
82
- if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
83
- logger.warn({
84
- code: 'MC_SECURITY_PATH_TRAVERSAL',
85
- message: 'Path traversal attempt blocked in returnPartialView',
86
- path: location
87
- });
88
- this.returnError(400, 'Invalid path');
89
- return '';
90
- }
91
-
92
- const actionUrl = path.resolve(MasterAction._master.root, location);
93
-
94
- // SECURITY: Ensure resolved path is within app root
95
- if (!actionUrl.startsWith(MasterAction._master.root)) {
96
- logger.warn({
97
- code: 'MC_SECURITY_PATH_TRAVERSAL',
98
- message: 'Path traversal blocked in returnPartialView',
99
- requestedPath: location,
100
- resolvedPath: actionUrl
101
- });
102
- this.returnError(403, 'Forbidden');
103
- return '';
104
- }
105
-
106
- try {
107
- const getAction = fileserver.readFileSync(actionUrl, 'utf8');
108
- if (MasterAction._master.overwrite.isTemplate){
109
- return MasterAction._master.overwrite.templateRender( data, "returnPartialView");
110
- }
111
- else{
112
- return temp.htmlBuilder(getAction, data);
113
- }
114
- } catch (error) {
115
- logger.error({
116
- code: 'MC_ERR_PARTIAL_VIEW',
117
- message: 'Failed to read partial view',
118
- path: location,
119
- error: error.message
120
- });
121
- this.returnError(404, 'View not found');
122
- return '';
123
- }
124
- }
61
+ // returnPartialView() removed - handled by view engine (register via master.useView())
125
62
 
126
63
  redirectBack(fallback){
127
64
  if(fallback === undefined){
@@ -202,154 +139,13 @@ class MasterAction{
202
139
  MasterAction._master.router._call(requestObj);
203
140
  }
204
141
 
205
- // this will allow static pages without master view
206
- returnViewWithoutMaster(location, data){
207
- var masterView = null;
208
- this.params = this.params === undefined ? {} : this.params;
209
- this.params = tools.combineObjects(data, this.params);
210
- var func = MasterAction._master.viewList;
211
- this.params = tools.combineObjects(this.params, func);
212
- // Prefer page.js module if present (no legacy .html file)
213
- try {
214
- const controller = this.__currentRoute.toController;
215
- const action = this.__currentRoute.toAction;
216
- const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
217
- if (fileserver.existsSync(pageModuleAbs)) {
218
- if (this._renderPageModule(controller, action, data)) { return; }
219
- }
220
- } catch (_) {}
142
+ // returnViewWithoutMaster() removed - handled by view engine (register via master.useView())
221
143
 
222
- var actionUrl = (location === undefined) ? this.__currentRoute.root + "/app/views/" + this.__currentRoute.toController + "/" + this.__currentRoute.toAction + ".html" : MasterAction._master.root + location;
223
- var actionView = fileserver.readFileSync(actionUrl, 'utf8');
224
- if (MasterAction._master.overwrite.isTemplate){
225
- masterView = MasterAction._master.overwrite.templateRender(data, "returnViewWithoutMaster");
226
- }
227
- else{
228
- masterView = temp.htmlBuilder(actionView, data);
229
- }
230
- if (!this.__requestObject.response._headerSent) {
231
- const send = (htmlOut) => {
232
- try {
233
- this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
234
- this.__requestObject.response.end(htmlOut);
235
- } catch (e) {
236
- // Fallback in case of double send
237
- }
238
- };
239
- try {
240
- Promise.resolve(compileWebComponentsHTML(masterView))
241
- .then(send)
242
- .catch(() => send(masterView));
243
- } catch (_) {
244
- send(masterView);
245
- }
246
- }
247
- }
144
+ // returnViewWithoutEngine() removed - handled by view engine (register via master.useView())
248
145
 
249
- returnViewWithoutEngine(location){
250
- // SECURITY: Validate path to prevent traversal attacks
251
- if (!location || location.includes('..') || location.includes('~') || path.isAbsolute(location)) {
252
- logger.warn({
253
- code: 'MC_SECURITY_PATH_TRAVERSAL',
254
- message: 'Path traversal attempt blocked in returnViewWithoutEngine',
255
- path: location
256
- });
257
- this.returnError(400, 'Invalid path');
258
- return;
259
- }
260
-
261
- const actionUrl = path.resolve(MasterAction._master.root, location);
262
-
263
- // SECURITY: Ensure resolved path is within app root
264
- if (!actionUrl.startsWith(MasterAction._master.root)) {
265
- logger.warn({
266
- code: 'MC_SECURITY_PATH_TRAVERSAL',
267
- message: 'Path traversal blocked in returnViewWithoutEngine',
268
- requestedPath: location,
269
- resolvedPath: actionUrl
270
- });
271
- this.returnError(403, 'Forbidden');
272
- return;
273
- }
274
-
275
- try {
276
- const masterView = fileserver.readFileSync(actionUrl, 'utf8');
277
- if (!this.__requestObject.response._headerSent) {
278
- this.__requestObject.response.writeHead(200, {'Content-Type': 'text/html'});
279
- this.__requestObject.response.end(masterView);
280
- }
281
- } catch (error) {
282
- logger.error({
283
- code: 'MC_ERR_VIEW_READ',
284
- message: 'Failed to read view file',
285
- path: location,
286
- error: error.message
287
- });
288
- this.returnError(404, 'View not found');
289
- }
290
- }
291
-
292
- returnReact(data, location){
293
-
294
- var masterView = null;
295
- data = data === undefined ? {} : data;
296
- this.params = this.params === undefined ? {} : this.params;
297
- this.params = tools.combineObjects(data, this.params);
298
- var func = MasterAction._master.viewList;
299
- this.params = tools.combineObjects(this.params, func);
300
- var html = MasterAction._master.reactView.compile(this.__currentRoute.toController, this.__currentRoute.toAction, this.__currentRoute.root);
301
-
302
- }
303
-
304
- returnView(data, location){
305
-
306
- var masterView = null;
307
- data = data === undefined ? {} : data;
308
- this.params = this.params === undefined ? {} : this.params;
309
- this.params = tools.combineObjects(data, this.params);
310
- var func = MasterAction._master.viewList;
311
- this.params = tools.combineObjects(this.params, func);
312
- // Prefer page.js module if present (no legacy .html file)
313
- try {
314
- const controller = this.__currentRoute.toController;
315
- const action = this.__currentRoute.toAction;
316
- const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
317
- if (fileserver.existsSync(pageModuleAbs)) {
318
- if (this._renderPageModule(controller, action, data)) { return; }
319
- }
320
- } catch (_) {}
146
+ // returnReact() removed - handled by view engine (register via master.useView())
321
147
 
322
- var viewUrl = (location === undefined || location === "" || location === null) ? this.__currentRoute.root + "/app/views/" + this.__currentRoute.toController + "/" + this.__currentRoute.toAction + ".html" : MasterAction._master.root + location;
323
- var viewFile = fileserver.readFileSync(viewUrl,'utf8');
324
- var masterFile = fileserver.readFileSync(this.__currentRoute.root + "/app/views/layouts/master.html", 'utf8');
325
- if (MasterAction._master.overwrite.isTemplate){
326
- masterView = MasterAction._master.overwrite.templateRender(this.params, "returnView");
327
- }
328
- else{
329
- var childView = temp.htmlBuilder(viewFile, this.params);
330
- this.params.yield = childView;
331
- masterView = temp.htmlBuilder(masterFile, this.params);
332
- }
333
-
334
- if (!this.__response._headerSent) {
335
- const send = (htmlOut) => {
336
- try {
337
- this.__response.writeHead(200, {'Content-Type': 'text/html'});
338
- this.__response.end(htmlOut);
339
- } catch (e) {
340
- // Fallback in case of double send
341
- }
342
- };
343
- try {
344
- Promise.resolve(compileWebComponentsHTML(masterView))
345
- .then(send)
346
- .catch(() => send(masterView));
347
- } catch (_) {
348
- send(masterView);
349
- }
350
- }
351
-
352
- }
148
+ // returnView() removed - handled by view engine (register via master.useView())
353
149
 
354
150
  close(response, code, content, end){
355
151
  response.writeHead(code, content.type);
@@ -381,58 +177,9 @@ class MasterAction{
381
177
  return false;
382
178
  }
383
179
 
384
- // Render using a page.js Web Component module when present
385
- _renderPageModule(controller, action, data) {
386
- try {
387
- const pageModuleAbs = path.join(MasterAction._master.root, 'app/views', controller, action, 'page.js');
388
- const layoutModuleAbs = path.join(MasterAction._master.root, 'app/views', 'layouts', 'master.js');
389
- const stylesPath = '/app/assets/stylesheets/output.css';
390
- const pageTag = `home-${action}-page`;
391
-
392
- const htmlDoc =
393
- `<!DOCTYPE html>
394
- <html lang="en">
395
- <head>
396
- <meta charset="utf-8"/>
397
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
398
- <title>${controller}/${action}</title>
399
- <link rel="stylesheet" href="${stylesPath}"/>
400
- </head>
401
- <body class="geist-variable antialiased">
402
- <root-layout>
403
- <${pageTag}></${pageTag}>
404
- </root-layout>
405
- <script type="module" src="/app/views/layouts/master.js"></script>
406
- <script type="module" src="/app/views/${controller}/${action}/page.js"></script>
407
- </body>
408
- </html>`;
409
-
410
- const send = (htmlOut) => {
411
- try {
412
- const res = this.__response || (this.__requestObject && this.__requestObject.response);
413
- if (res && !res._headerSent) {
414
- res.writeHead(200, { 'Content-Type': 'text/html' });
415
- res.end(htmlOut);
416
- }
417
- } catch (_) {}
418
- };
419
-
420
- Promise
421
- .resolve(require('./ssr/runtime-ssr.cjs')(htmlDoc, [layoutModuleAbs, pageModuleAbs]))
422
- .then(send)
423
- .catch(() => send(htmlDoc));
424
- } catch (e) {
425
- // Fallback to legacy view if something goes wrong
426
- console.warn('[SSR] _renderPageModule failed:', e && e.message);
427
- return false;
428
- }
429
- return true;
430
- }
180
+ // _renderPageModule() removed - handled by view engine (register via master.useView())
431
181
 
432
- // Delegate to standard Enhance-based SSR only
433
- returnWebComponent(data) {
434
- this.returnView(data);
435
- }
182
+ // returnWebComponent() removed - handled by view engine (register via master.useView())
436
183
 
437
184
  // ==================== Security Methods ====================
438
185
 
package/MasterControl.js CHANGED
@@ -10,6 +10,7 @@ var fs = require('fs');
10
10
  var url = require('url');
11
11
  var path = require('path');
12
12
  var globSearch = require("glob");
13
+ var crypto = require('crypto'); // CRITICAL FIX: For ETag generation
13
14
 
14
15
  // Enhanced error handling - setup global handlers
15
16
  const { setupGlobalErrorHandlers } = require('./error/MasterErrorMiddleware');
@@ -67,6 +68,7 @@ class MasterControl {
67
68
  _hstsMaxAge = 31536000 // 1 year default
68
69
  _hstsIncludeSubDomains = true
69
70
  _hstsPreload = false
71
+ _viewEngine = null // Pluggable view engine (MasterView, EJS, Pug, etc.)
70
72
 
71
73
  #loadTransientListClasses(name, params){
72
74
  Object.defineProperty(this.requestList, name, {
@@ -124,35 +126,127 @@ class MasterControl {
124
126
  }
125
127
  }
126
128
 
129
+ /**
130
+ * Initialize prototype pollution protection
131
+ * SECURITY: Prevents malicious modification of Object/Array prototypes
132
+ */
133
+ _initPrototypePollutionProtection() {
134
+ // Only freeze in production to allow for easier debugging in development
135
+ const isProduction = process.env.NODE_ENV === 'production';
136
+
137
+ if (isProduction) {
138
+ // Freeze prototypes to prevent prototype pollution attacks
139
+ try {
140
+ Object.freeze(Object.prototype);
141
+ Object.freeze(Array.prototype);
142
+ Object.freeze(Function.prototype);
143
+
144
+ logger.info({
145
+ code: 'MC_SECURITY_PROTOTYPE_FROZEN',
146
+ message: 'Prototypes frozen in production mode for security'
147
+ });
148
+ } catch (err) {
149
+ logger.warn({
150
+ code: 'MC_SECURITY_FREEZE_FAILED',
151
+ message: 'Failed to freeze prototypes',
152
+ error: err.message
153
+ });
154
+ }
155
+ }
156
+
157
+ // Add prototype pollution detection utility
158
+ this._detectPrototypePollution = (obj) => {
159
+ if (!obj || typeof obj !== 'object') {
160
+ return false;
161
+ }
162
+
163
+ const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
164
+
165
+ for (const key of dangerousKeys) {
166
+ if (key in obj) {
167
+ logger.error({
168
+ code: 'MC_SECURITY_PROTOTYPE_POLLUTION',
169
+ message: `Prototype pollution detected: ${key} in object`,
170
+ severity: 'CRITICAL'
171
+ });
172
+ return true;
173
+ }
174
+ }
175
+
176
+ return false;
177
+ };
178
+
179
+ console.log('[MasterControl] Prototype pollution protection initialized');
180
+ }
181
+
127
182
  // extends class methods to be used inside of the view class using the THIS keyword
128
183
  extendView( name, element){
129
184
  element = new element();
130
- var $that = this;
131
- var propertyNames = Object.getOwnPropertyNames( element.__proto__);
185
+ const propertyNames = Object.getOwnPropertyNames(element.__proto__);
132
186
  this.viewList[name] = {};
133
- for(var i in propertyNames){
134
- if(propertyNames[i] !== "constructor"){
135
- if (propertyNames.hasOwnProperty(i)) {
136
- $that.viewList[name][propertyNames[i]] = element[propertyNames[i]];
137
- }
187
+
188
+ // Fixed: Use for...of instead of for...in for array iteration
189
+ // Filter out 'constructor' and iterate efficiently
190
+ for (const propName of propertyNames) {
191
+ if (propName !== "constructor") {
192
+ this.viewList[name][propName] = element[propName];
138
193
  }
139
- };
194
+ }
140
195
  }
141
196
 
142
197
  // extends class methods to be used inside of the controller class using the THIS keyword
143
198
  extendController(element){
144
199
  element = new element();
145
- var $that = this;
146
- var propertyNames = Object.getOwnPropertyNames( element.__proto__);
147
- for(var i in propertyNames){
148
- if(propertyNames[i] !== "constructor"){
149
- if (propertyNames.hasOwnProperty(i)) {
150
- $that.controllerList[propertyNames[i]] = element[propertyNames[i]];
151
- }
200
+ const propertyNames = Object.getOwnPropertyNames(element.__proto__);
201
+
202
+ // Fixed: Use for...of instead of for...in for array iteration
203
+ // Filter out 'constructor' and iterate efficiently
204
+ for (const propName of propertyNames) {
205
+ if (propName !== "constructor") {
206
+ this.controllerList[propName] = element[propName];
152
207
  }
153
- };
208
+ }
154
209
  }
155
-
210
+
211
+ /**
212
+ * Register a view engine (MasterView, React, EJS, Pug, etc.)
213
+ * This allows for pluggable view rendering
214
+ *
215
+ * @param {Object|Function} ViewEngine - View engine class or instance
216
+ * @param {Object} options - Configuration options for the view engine
217
+ * @returns {MasterControl} - Returns this for chaining
218
+ *
219
+ * @example
220
+ * // Use MasterView (official view engine)
221
+ * const MasterView = require('masterview');
222
+ * master.useView(MasterView, { ssr: true });
223
+ *
224
+ * @example
225
+ * // Use EJS adapter
226
+ * const EJSAdapter = require('./adapters/ejs');
227
+ * master.useView(EJSAdapter);
228
+ */
229
+ useView(ViewEngine, options = {}) {
230
+ if (typeof ViewEngine === 'function') {
231
+ this._viewEngine = new ViewEngine(options);
232
+ } else {
233
+ this._viewEngine = ViewEngine;
234
+ }
235
+
236
+ // Let the view engine register itself
237
+ if (this._viewEngine && this._viewEngine.register) {
238
+ this._viewEngine.register(this);
239
+ }
240
+
241
+ logger.info({
242
+ code: 'MC_INFO_VIEW_ENGINE_REGISTERED',
243
+ message: 'View engine registered',
244
+ engine: ViewEngine.name || 'Custom'
245
+ });
246
+
247
+ return this;
248
+ }
249
+
156
250
  /*
157
251
  Services are created each time they are requested.
158
252
  It gets a new instance of the injected object, on each request of this object.
@@ -299,6 +393,9 @@ class MasterControl {
299
393
  try {
300
394
  var $that = this;
301
395
 
396
+ // SECURITY: Initialize prototype pollution protection
397
+ this._initPrototypePollutionProtection();
398
+
302
399
  // AUTO-LOAD internal framework modules
303
400
  // These are required for the framework to function and are loaded transparently
304
401
  const internalModules = {
@@ -313,10 +410,11 @@ class MasterControl {
313
410
  'MasterCors': './MasterCors',
314
411
  'SessionSecurity': './security/SessionSecurity',
315
412
  'MasterSocket': './MasterSocket',
316
- 'MasterHtml': './MasterHtml',
317
- 'MasterTemplate': './MasterTemplate',
318
- 'MasterTools': './MasterTools',
319
- 'TemplateOverwrite': './TemplateOverwrite'
413
+ 'MasterTools': './MasterTools'
414
+ // View modules removed - use master.useView(MasterView) instead
415
+ // 'MasterHtml': './MasterHtml',
416
+ // 'MasterTemplate': './MasterTemplate',
417
+ // 'TemplateOverwrite': './TemplateOverwrite'
320
418
  };
321
419
 
322
420
  // Explicit module registration (prevents circular dependency issues)
@@ -331,8 +429,8 @@ class MasterControl {
331
429
  'cors': { path: './MasterCors', exportName: 'MasterCors' },
332
430
  'socket': { path: './MasterSocket', exportName: 'MasterSocket' },
333
431
  'tempdata': { path: './MasterTemp', exportName: 'MasterTemp' },
334
- 'overwrite': { path: './TemplateOverwrite', exportName: 'TemplateOverwrite' },
335
432
  'session': { path: './security/SessionSecurity', exportName: 'MasterSessionSecurity' }
433
+ // 'overwrite' removed - will be provided by view engine (master.useView())
336
434
  };
337
435
 
338
436
  for (const [name, config] of Object.entries(moduleRegistry)) {
@@ -354,13 +452,12 @@ class MasterControl {
354
452
  // Legacy code uses master.sessions (plural), new API uses master.session (singular)
355
453
  $that.sessions = $that.session;
356
454
 
357
- // Load view and controller extensions (these extend prototypes, not master instance)
455
+ // Load controller extensions (these extend prototypes, not master instance)
358
456
  try {
359
457
  require('./MasterAction');
360
458
  require('./MasterActionFilters');
361
- require('./MasterHtml');
362
- require('./MasterTemplate');
363
459
  require('./MasterTools');
460
+ // View extensions (MasterHtml, MasterTemplate) removed - use master.useView() instead
364
461
  } catch (e) {
365
462
  console.error('[MasterControl] Failed to load extensions:', e.message);
366
463
  }
@@ -683,26 +780,107 @@ class MasterControl {
683
780
  }
684
781
  }
685
782
 
686
- // Read and serve the file
687
- fs.readFile(finalPath, function(err, data) {
688
- if (err) {
783
+ // CRITICAL FIX: Stream large files instead of reading into memory
784
+ // Files >1MB are streamed to prevent memory exhaustion and improve performance
785
+ const STREAM_THRESHOLD = 1 * 1024 * 1024; // 1MB
786
+ const fileSize = stats.isDirectory() ? fs.statSync(finalPath).size : stats.size;
787
+ const ext = path.extname(finalPath);
788
+ const mimeType = $that.router.findMimeType(ext);
789
+
790
+ // CRITICAL FIX: Generate ETag for caching (based on file stats)
791
+ // ETag format: "size-mtime" (weak ETag for better performance)
792
+ const fileStats = stats.isDirectory() ? fs.statSync(finalPath) : stats;
793
+ const etag = `W/"${fileStats.size}-${fileStats.mtime.getTime()}"`;
794
+
795
+ // CRITICAL FIX: Check If-None-Match header for 304 Not Modified
796
+ const clientETag = ctx.request.headers['if-none-match'];
797
+ if (clientETag === etag) {
798
+ // File hasn't changed, return 304 Not Modified
799
+ logger.debug({
800
+ code: 'MC_STATIC_304',
801
+ message: 'Returning 304 Not Modified',
802
+ path: finalPath,
803
+ etag: etag
804
+ });
805
+ ctx.response.statusCode = 304;
806
+ ctx.response.setHeader('ETag', etag);
807
+ ctx.response.end();
808
+ return;
809
+ }
810
+
811
+ // Set common headers for both streaming and buffered responses
812
+ ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
813
+ ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
814
+ ctx.response.setHeader('Content-Length', fileSize);
815
+
816
+ // CRITICAL FIX: Add caching headers
817
+ ctx.response.setHeader('ETag', etag);
818
+ ctx.response.setHeader('Last-Modified', fileStats.mtime.toUTCString());
819
+
820
+ // Cache-Control based on file type
821
+ const cacheableExtensions = ['.js', '.css', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico'];
822
+ const isCacheable = cacheableExtensions.includes(ext.toLowerCase());
823
+
824
+ if (isCacheable) {
825
+ // PERFORMANCE: Cache static assets for 1 year (immutable pattern)
826
+ // Use versioned URLs (e.g., app.v123.js) for cache-busting
827
+ ctx.response.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
828
+ } else {
829
+ // SECURITY: Dynamic content should revalidate
830
+ ctx.response.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
831
+ }
832
+
833
+ if (fileSize > STREAM_THRESHOLD) {
834
+ // PERFORMANCE: Stream large files (>1MB) to avoid memory issues
835
+ logger.debug({
836
+ code: 'MC_STATIC_STREAMING',
837
+ message: 'Streaming large static file',
838
+ path: finalPath,
839
+ size: fileSize
840
+ });
841
+
842
+ const readStream = fs.createReadStream(finalPath);
843
+
844
+ readStream.on('error', (err) => {
689
845
  logger.error({
690
- code: 'MC_ERR_FILE_READ',
691
- message: 'Error reading static file',
846
+ code: 'MC_ERR_STREAM_READ',
847
+ message: 'Error streaming static file',
692
848
  path: finalPath,
693
849
  error: err.message
694
850
  });
695
- ctx.response.statusCode = 500;
696
- ctx.response.setHeader('Content-Type', 'text/plain');
697
- ctx.response.end('Internal Server Error');
698
- } else {
699
- const ext = path.extname(finalPath);
700
- const mimeType = $that.router.findMimeType(ext);
701
- ctx.response.setHeader('Content-Type', mimeType || 'application/octet-stream');
702
- ctx.response.setHeader('X-Content-Type-Options', 'nosniff');
703
- ctx.response.end(data);
704
- }
705
- });
851
+
852
+ // Only send error if headers not sent
853
+ if (!ctx.response.headersSent) {
854
+ ctx.response.statusCode = 500;
855
+ ctx.response.setHeader('Content-Type', 'text/plain');
856
+ ctx.response.end('Internal Server Error');
857
+ } else {
858
+ // Connection already started, just close it
859
+ ctx.response.end();
860
+ }
861
+ });
862
+
863
+ // Pipe the file stream to the response
864
+ readStream.pipe(ctx.response);
865
+
866
+ } else {
867
+ // PERFORMANCE: Small files (<1MB) can be buffered for better caching
868
+ fs.readFile(finalPath, function(err, data) {
869
+ if (err) {
870
+ logger.error({
871
+ code: 'MC_ERR_FILE_READ',
872
+ message: 'Error reading static file',
873
+ path: finalPath,
874
+ error: err.message
875
+ });
876
+ ctx.response.statusCode = 500;
877
+ ctx.response.setHeader('Content-Type', 'text/plain');
878
+ ctx.response.end('Internal Server Error');
879
+ } else {
880
+ ctx.response.end(data);
881
+ }
882
+ });
883
+ }
706
884
  });
707
885
 
708
886
  return; // Terminal - don't call next()
@@ -734,9 +912,14 @@ class MasterControl {
734
912
  });
735
913
 
736
914
  // 4. Load Scoped Services (per request - always needed)
915
+ // Cache keys for performance (computed once, not on every request)
916
+ const scopedKeys = Object.keys($that._scopedList);
917
+
737
918
  $that.pipeline.use(async (ctx, next) => {
738
- for (var key in $that._scopedList) {
739
- var className = $that._scopedList[key];
919
+ // Fixed: Use cached keys with direct array iteration (faster & safer)
920
+ for (let i = 0; i < scopedKeys.length; i++) {
921
+ const key = scopedKeys[i];
922
+ const className = $that._scopedList[key];
740
923
  $that.requestList[key] = new className();
741
924
  }
742
925
  await next();