mastercontroller 1.2.13 → 1.3.0

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 (38) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/MasterAction.js +7 -7
  3. package/MasterControl.js +192 -122
  4. package/MasterCors.js +29 -0
  5. package/MasterHtml.js +5 -5
  6. package/MasterPipeline.js +344 -0
  7. package/MasterRouter.js +59 -29
  8. package/MasterSession.js +19 -0
  9. package/MasterTemplate.js +3 -3
  10. package/MasterTimeout.js +332 -0
  11. package/README.md +1496 -36
  12. package/docs/timeout-and-error-handling.md +712 -0
  13. package/{MasterError.js → error/MasterError.js} +2 -2
  14. package/{MasterErrorLogger.js → error/MasterErrorLogger.js} +1 -1
  15. package/{MasterErrorMiddleware.js → error/MasterErrorMiddleware.js} +2 -2
  16. package/error/MasterErrorRenderer.js +529 -0
  17. package/{ssr → error}/SSRErrorHandler.js +2 -2
  18. package/{MasterCache.js → monitoring/MasterCache.js} +2 -2
  19. package/{MasterMemoryMonitor.js → monitoring/MasterMemoryMonitor.js} +2 -2
  20. package/{MasterProfiler.js → monitoring/MasterProfiler.js} +2 -2
  21. package/{ssr → monitoring}/PerformanceMonitor.js +2 -2
  22. package/package.json +5 -5
  23. package/{EventHandlerValidator.js → security/EventHandlerValidator.js} +3 -3
  24. package/{MasterSanitizer.js → security/MasterSanitizer.js} +2 -2
  25. package/{MasterValidator.js → security/MasterValidator.js} +2 -2
  26. package/{SecurityMiddleware.js → security/SecurityMiddleware.js} +75 -3
  27. package/{SessionSecurity.js → security/SessionSecurity.js} +2 -2
  28. package/ssr/hydration-client.js +3 -3
  29. package/ssr/runtime-ssr.cjs +9 -9
  30. package/MasterBenchmark.js +0 -89
  31. package/MasterBuildOptimizer.js +0 -376
  32. package/MasterBundleAnalyzer.js +0 -108
  33. package/ssr/HTMLUtils.js +0 -15
  34. /package/{ssr → error}/ErrorBoundary.js +0 -0
  35. /package/{ssr → error}/HydrationMismatch.js +0 -0
  36. /package/{MasterBackendErrorHandler.js → error/MasterBackendErrorHandler.js} +0 -0
  37. /package/{MasterErrorHandler.js → error/MasterErrorHandler.js} +0 -0
  38. /package/{CSPConfig.js → security/CSPConfig.js} +0 -0
@@ -0,0 +1,344 @@
1
+ // MasterPipeline - Middleware Pipeline System
2
+ // version 1.0
3
+
4
+ var master = require('./MasterControl');
5
+ const { logger } = require('./error/MasterErrorLogger');
6
+
7
+ class MasterPipeline {
8
+ constructor() {
9
+ this.middleware = [];
10
+ this.errorHandlers = [];
11
+ }
12
+
13
+ /**
14
+ * Use: Add middleware that processes request/response
15
+ *
16
+ * Middleware signature: async (ctx, next) => { await next(); }
17
+ * - ctx: Request context { request, response, params, state, ... }
18
+ * - next: Function to call next middleware in chain
19
+ *
20
+ * Example:
21
+ * master.use(async (ctx, next) => {
22
+ * console.log('Before');
23
+ * await next();
24
+ * console.log('After');
25
+ * });
26
+ *
27
+ * @param {Function} middleware - Middleware function
28
+ * @returns {MasterPipeline} - For chaining
29
+ */
30
+ use(middleware) {
31
+ if (typeof middleware !== 'function') {
32
+ throw new Error('Middleware must be a function');
33
+ }
34
+
35
+ this.middleware.push({
36
+ type: 'use',
37
+ handler: middleware,
38
+ path: null
39
+ });
40
+
41
+ return this; // Chainable
42
+ }
43
+
44
+ /**
45
+ * Run: Add terminal middleware that ends the pipeline
46
+ *
47
+ * Terminal middleware signature: async (ctx) => { /* send response */ }
48
+ * - Does NOT call next()
49
+ * - Must send response
50
+ *
51
+ * Example:
52
+ * master.run(async (ctx) => {
53
+ * ctx.response.end('Hello World');
54
+ * });
55
+ *
56
+ * @param {Function} middleware - Terminal middleware function
57
+ * @returns {MasterPipeline} - For chaining
58
+ */
59
+ run(middleware) {
60
+ if (typeof middleware !== 'function') {
61
+ throw new Error('Terminal middleware must be a function');
62
+ }
63
+
64
+ this.middleware.push({
65
+ type: 'run',
66
+ handler: middleware,
67
+ path: null
68
+ });
69
+
70
+ return this; // Chainable
71
+ }
72
+
73
+ /**
74
+ * Map: Conditionally execute middleware based on path
75
+ *
76
+ * Map signature: (path, configure)
77
+ * - path: String or RegExp to match request path
78
+ * - configure: Function that receives a branch pipeline
79
+ *
80
+ * Example:
81
+ * master.map('/api/*', (api) => {
82
+ * api.use(authMiddleware);
83
+ * api.use(jsonMiddleware);
84
+ * });
85
+ *
86
+ * @param {String|RegExp} path - Path pattern to match
87
+ * @param {Function} configure - Function to configure branch pipeline
88
+ * @returns {MasterPipeline} - For chaining
89
+ */
90
+ map(path, configure) {
91
+ if (typeof configure !== 'function') {
92
+ throw new Error('Map configuration must be a function');
93
+ }
94
+
95
+ // Create sub-pipeline for this branch
96
+ const branch = new MasterPipeline();
97
+ configure(branch);
98
+
99
+ // Wrap branch in conditional middleware
100
+ const conditionalMiddleware = async (ctx, next) => {
101
+ const requestPath = ctx.pathName || ctx.request.url;
102
+
103
+ if (this._pathMatches(requestPath, path)) {
104
+ // Execute branch pipeline
105
+ await branch.execute(ctx);
106
+ // After branch completes, continue main pipeline
107
+ await next();
108
+ } else {
109
+ // Skip branch, continue main pipeline
110
+ await next();
111
+ }
112
+ };
113
+
114
+ this.middleware.push({
115
+ type: 'map',
116
+ handler: conditionalMiddleware,
117
+ path: path
118
+ });
119
+
120
+ return this; // Chainable
121
+ }
122
+
123
+ /**
124
+ * UseError: Add error handling middleware
125
+ *
126
+ * Error middleware signature: async (error, ctx, next) => { }
127
+ * - error: The caught error
128
+ * - ctx: Request context
129
+ * - next: Pass to next error handler or rethrow
130
+ *
131
+ * Example:
132
+ * master.useError(async (err, ctx, next) => {
133
+ * if (err.statusCode === 404) {
134
+ * ctx.response.statusCode = 404;
135
+ * ctx.response.end('Not Found');
136
+ * } else {
137
+ * await next(); // Pass to next error handler
138
+ * }
139
+ * });
140
+ *
141
+ * @param {Function} handler - Error handler function
142
+ * @returns {MasterPipeline} - For chaining
143
+ */
144
+ useError(handler) {
145
+ if (typeof handler !== 'function') {
146
+ throw new Error('Error handler must be a function');
147
+ }
148
+
149
+ this.errorHandlers.push(handler);
150
+ return this; // Chainable
151
+ }
152
+
153
+ /**
154
+ * Execute: Run the middleware pipeline for a request
155
+ *
156
+ * Called internally by the framework for each request
157
+ *
158
+ * @param {Object} context - Request context
159
+ */
160
+ async execute(context) {
161
+ let index = 0;
162
+
163
+ // Create the next function for middleware chain
164
+ const next = async () => {
165
+ // If we've run all middleware, we're done
166
+ if (index >= this.middleware.length) {
167
+ return;
168
+ }
169
+
170
+ const current = this.middleware[index++];
171
+
172
+ try {
173
+ if (current.type === 'run') {
174
+ // Terminal middleware - don't pass next
175
+ await current.handler(context);
176
+ } else {
177
+ // Regular middleware - pass next
178
+ await current.handler(context, next);
179
+ }
180
+ } catch (error) {
181
+ // Error occurred, run error handlers
182
+ await this._handleError(error, context);
183
+ }
184
+ };
185
+
186
+ // Start the pipeline
187
+ await next();
188
+ }
189
+
190
+ /**
191
+ * Handle errors through error handler chain
192
+ *
193
+ * @param {Error} error - The error that occurred
194
+ * @param {Object} context - Request context
195
+ */
196
+ async _handleError(error, context) {
197
+ let errorIndex = 0;
198
+
199
+ const nextError = async () => {
200
+ if (errorIndex >= this.errorHandlers.length) {
201
+ // No more error handlers, log and send generic error
202
+ logger.error({
203
+ code: 'MC_ERR_UNHANDLED',
204
+ message: 'Unhandled error in middleware pipeline',
205
+ error: error.message,
206
+ stack: error.stack
207
+ });
208
+
209
+ if (!context.response.headersSent) {
210
+ context.response.statusCode = 500;
211
+ context.response.end('Internal Server Error');
212
+ }
213
+ return;
214
+ }
215
+
216
+ const handler = this.errorHandlers[errorIndex++];
217
+
218
+ try {
219
+ await handler(error, context, nextError);
220
+ } catch (handlerError) {
221
+ // Error in error handler
222
+ logger.error({
223
+ code: 'MC_ERR_ERROR_HANDLER_FAILED',
224
+ message: 'Error handler threw an error',
225
+ error: handlerError.message
226
+ });
227
+ await nextError();
228
+ }
229
+ };
230
+
231
+ await nextError();
232
+ }
233
+
234
+ /**
235
+ * Check if request path matches the map path pattern
236
+ *
237
+ * @param {String} requestPath - The request path
238
+ * @param {String|RegExp} pattern - The pattern to match
239
+ * @returns {Boolean} - True if matches
240
+ */
241
+ _pathMatches(requestPath, pattern) {
242
+ // Normalize paths (ensure leading slash)
243
+ requestPath = '/' + requestPath.replace(/^\/|\/$/g, '');
244
+
245
+ if (typeof pattern === 'string') {
246
+ pattern = '/' + pattern.replace(/^\/|\/$/g, '');
247
+
248
+ // Wildcard support: /api/* matches /api/users, /api/posts, etc.
249
+ if (pattern.endsWith('/*')) {
250
+ const prefix = pattern.slice(0, -2);
251
+ return requestPath === prefix || requestPath.startsWith(prefix + '/');
252
+ }
253
+
254
+ // Exact or prefix match
255
+ return requestPath === pattern || requestPath.startsWith(pattern + '/');
256
+ }
257
+
258
+ if (pattern instanceof RegExp) {
259
+ return pattern.test(requestPath);
260
+ }
261
+
262
+ return false;
263
+ }
264
+
265
+ /**
266
+ * Discover and load middleware from folders
267
+ *
268
+ * @param {String|Object} options - Folder path or options object
269
+ */
270
+ discoverMiddleware(options) {
271
+ const fs = require('fs');
272
+ const path = require('path');
273
+
274
+ const folders = typeof options === 'string'
275
+ ? [options]
276
+ : (options.folders || ['middleware']);
277
+
278
+ folders.forEach(folder => {
279
+ const dir = path.join(master.root, folder);
280
+ if (!fs.existsSync(dir)) {
281
+ console.warn(`[Middleware] Folder not found: ${folder}`);
282
+ return;
283
+ }
284
+
285
+ const files = fs.readdirSync(dir)
286
+ .filter(file => file.endsWith('.js'))
287
+ .sort(); // Alphabetical order
288
+
289
+ files.forEach(file => {
290
+ try {
291
+ const middlewarePath = path.join(dir, file);
292
+ const middleware = require(middlewarePath);
293
+
294
+ // Support two patterns:
295
+ // Pattern 1: module.exports = async (ctx, next) => {}
296
+ if (typeof middleware === 'function') {
297
+ this.use(middleware);
298
+ }
299
+ // Pattern 2: module.exports = { register: (master) => {} }
300
+ else if (middleware.register && typeof middleware.register === 'function') {
301
+ middleware.register(master);
302
+ }
303
+ else {
304
+ console.warn(`[Middleware] Invalid export in ${folder}/${file}`);
305
+ return;
306
+ }
307
+
308
+ console.log(`[Middleware] Loaded: ${folder}/${file}`);
309
+ } catch (err) {
310
+ console.error(`[Middleware] Failed to load ${folder}/${file}:`, err.message);
311
+ }
312
+ });
313
+ });
314
+ }
315
+
316
+ /**
317
+ * Clear all middleware (useful for testing)
318
+ */
319
+ clear() {
320
+ this.middleware = [];
321
+ this.errorHandlers = [];
322
+ }
323
+
324
+ /**
325
+ * Inspect pipeline (for debugging)
326
+ *
327
+ * @returns {Object} - Pipeline information
328
+ */
329
+ inspect() {
330
+ return {
331
+ middlewareCount: this.middleware.length,
332
+ errorHandlerCount: this.errorHandlers.length,
333
+ middleware: this.middleware.map((m, i) => ({
334
+ index: i,
335
+ type: m.type,
336
+ path: m.path,
337
+ name: m.handler.name || 'anonymous'
338
+ }))
339
+ };
340
+ }
341
+ }
342
+
343
+ // Register with master
344
+ master.extend("pipeline", MasterPipeline);
package/MasterRouter.js CHANGED
@@ -1,4 +1,4 @@
1
- // version 0.0.249
1
+ // version 0.0.250
2
2
 
3
3
  var master = require('./MasterControl');
4
4
  var toolClass = require('./MasterTools');
@@ -8,13 +8,13 @@ var currentRoute = {};
8
8
  var tools = new toolClass();
9
9
 
10
10
  // Enhanced error handling
11
- const { handleRoutingError, handleControllerError, sendErrorResponse } = require('./MasterBackendErrorHandler');
12
- const { logger } = require('./MasterErrorLogger');
13
- const { performanceTracker, errorHandlerMiddleware } = require('./MasterErrorMiddleware');
11
+ const { handleRoutingError, handleControllerError, sendErrorResponse } = require('./error/MasterBackendErrorHandler');
12
+ const { logger } = require('./error/MasterErrorLogger');
13
+ const { performanceTracker, errorHandlerMiddleware } = require('./error/MasterErrorMiddleware');
14
14
 
15
15
  // Security - Input validation and sanitization
16
- const { validator, detectPathTraversal, detectSQLInjection, detectCommandInjection } = require('./MasterValidator');
17
- const { escapeHTML } = require('./MasterSanitizer');
16
+ const { validator, detectPathTraversal, detectSQLInjection, detectCommandInjection } = require('./security/MasterValidator');
17
+ const { escapeHTML } = require('./security/MasterSanitizer');
18
18
 
19
19
  const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.master === 'development';
20
20
 
@@ -135,10 +135,16 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
135
135
  try {
136
136
  requestObject.toController = routeList[item].toController;
137
137
  requestObject.toAction = routeList[item].toAction;
138
- var pathObj = normalizePaths(requestObject.pathName, routeList[item].path, requestObject.params);
138
+
139
+ // FIX: Create a clean copy of params for each route test to prevent parameter pollution
140
+ // This prevents parameters from non-matching routes from accumulating in requestObject.params
141
+ var testParams = Object.assign({}, requestObject.params);
142
+ var pathObj = normalizePaths(requestObject.pathName, routeList[item].path, testParams);
139
143
 
140
144
  // if we find the route that matches the request
141
145
  if(pathObj.requestPath === pathObj.routePath && routeList[item].type === requestObject.type){
146
+ // Only commit the extracted params if this route actually matches
147
+ requestObject.params = testParams;
142
148
 
143
149
  // call Constraint
144
150
  if(typeof routeList[item].constraint === "function"){
@@ -197,6 +203,8 @@ const isDevelopment = process.env.NODE_ENV !== 'production' && process.env.maste
197
203
 
198
204
  if(pathObj.requestPath === pathObj.routePath && "options" ===requestObject.type.toLowerCase()){
199
205
  // this means that the request is correct but its an options request means its the browser checking to see if the request is allowed
206
+ // Commit the params for OPTIONS requests too
207
+ requestObject.params = testParams;
200
208
  requestObject.response.writeHead(200, {'Content-Type': 'application/json'});
201
209
  requestObject.response.end(JSON.stringify({"done": "true"}));
202
210
  return true;
@@ -238,88 +246,110 @@ var loadScopedListClasses = function(){
238
246
  };
239
247
 
240
248
 
249
+ /**
250
+ * Normalize route path: lowercase segments but preserve param names
251
+ *
252
+ * @param {String} path - Route path like "/Period/:periodId/Items/:itemId"
253
+ * @returns {String} - Normalized: "period/:periodId/items/:itemId"
254
+ */
255
+ function normalizeRoutePath(path) {
256
+ const trimmed = path.replace(/^\/|\/$/g, '');
257
+ const segments = trimmed.split('/');
258
+
259
+ const normalized = segments.map(segment => {
260
+ // Preserve parameter names (start with :)
261
+ if (segment.startsWith(':')) {
262
+ return segment;
263
+ }
264
+ // Lowercase path segments
265
+ return segment.toLowerCase();
266
+ });
267
+
268
+ return normalized.join('/');
269
+ }
270
+
241
271
  class MasterRouter {
242
272
  currentRouteName = null
243
273
  _routes = {}
244
-
274
+
245
275
  start(){
246
276
  var $that = this;
247
277
  return {
248
278
  route : function(path, toPath, type, constraint){ // function to add to list of routes
249
-
279
+
250
280
  var pathList = toPath.replace(/^\/|\/$/g, '').split("#");
251
-
281
+
252
282
  var route = {
253
283
  type: type.toLowerCase(),
254
- path: path.replace(/^\/|\/$/g, '').toLowerCase(),
284
+ path: normalizeRoutePath(path),
255
285
  toController :pathList[0].replace(/^\/|\/$/g, ''),
256
286
  toAction: pathList[1],
257
287
  constraint : constraint
258
288
  };
259
-
289
+
260
290
  $that._routes[$that.currentRouteName].routes.push(route);
261
-
291
+
262
292
  },
263
293
 
264
294
  resources: function(routeName){ // function to add to list of routes using resources bulk
265
-
295
+
266
296
 
267
297
  $that._routes[$that.currentRouteName].routes.push({
268
298
  type: "get",
269
- path: routeName.toLowerCase(),
299
+ path: normalizeRoutePath(routeName),
270
300
  toController :routeName,
271
301
  toAction: "index",
272
302
  constraint : null
273
303
  });
274
-
304
+
275
305
  $that._routes[$that.currentRouteName].routes.push({
276
306
  type: "get",
277
- path: routeName.toLowerCase(),
307
+ path: normalizeRoutePath(routeName),
278
308
  toController :routeName,
279
309
  toAction: "new",
280
310
  constraint : null
281
311
  });
282
-
312
+
283
313
  $that._routes[$that.currentRouteName].routes.push({
284
314
  type: "post",
285
- path: routeName.toLowerCase(),
315
+ path: normalizeRoutePath(routeName),
286
316
  toController :routeName,
287
317
  toAction: "create",
288
318
  constraint : null
289
319
  });
290
-
320
+
291
321
  $that._routes[$that.currentRouteName].routes.push({
292
322
  // pages/3
293
323
  type: "get",
294
- path: routeName.toLowerCase() + "/:id",
324
+ path: normalizeRoutePath(routeName + "/:id"),
295
325
  toController :routeName,
296
326
  toAction: "show",
297
327
  constraint : null
298
328
  });
299
-
329
+
300
330
  $that._routes[$that.currentRouteName].routes.push({
301
331
  type: "get",
302
- path: routeName.toLowerCase() + "/:id/" + "edit",
332
+ path: normalizeRoutePath(routeName + "/:id/edit"),
303
333
  toController :routeName,
304
334
  toAction: "edit",
305
- constraint : null
335
+ constraint : null
306
336
  });
307
-
337
+
308
338
  $that._routes[$that.currentRouteName].routes.push({
309
339
  type: "put",
310
- path: routeName.toLowerCase() + "/:id",
340
+ path: normalizeRoutePath(routeName + "/:id"),
311
341
  toController :routeName,
312
342
  toAction: "update",
313
343
  constraint : null
314
344
  });
315
-
345
+
316
346
  $that._routes[$that.currentRouteName].routes.push({
317
347
  type: "delete",
318
- path: routeName.toLowerCase() + "/:id",
348
+ path: normalizeRoutePath(routeName + "/:id"),
319
349
  toController :routeName,
320
350
  toAction: "destroy",
321
351
  constraint : null
322
- });
352
+ });
323
353
  }
324
354
  }
325
355
  }
package/MasterSession.js CHANGED
@@ -35,6 +35,11 @@ class MasterSession{
35
35
  this.options.secret = TID;
36
36
  }
37
37
 
38
+ // Auto-register with pipeline if available
39
+ if (master.pipeline) {
40
+ master.pipeline.use(this.middleware());
41
+ }
42
+
38
43
  return {
39
44
  setPath : function(path){
40
45
  $that.options.path = path === undefined ? '/' : path;
@@ -184,6 +189,20 @@ class MasterSession{
184
189
  return -1;
185
190
  }
186
191
  }
192
+
193
+ /**
194
+ * Get session middleware for the pipeline
195
+ * Sessions are accessed lazily via master.sessions in controllers
196
+ */
197
+ middleware() {
198
+ var $that = this;
199
+
200
+ return async (ctx, next) => {
201
+ // Sessions are available via master.sessions.get/set in controllers
202
+ // No action needed here - just continue pipeline
203
+ await next();
204
+ };
205
+ }
187
206
  }
188
207
 
189
208
  master.extend("sessions", MasterSession);
package/MasterTemplate.js CHANGED
@@ -1,10 +1,10 @@
1
- // version 0.0.4
1
+ // version 0.0.5
2
2
  // https://github.com/WebReflection/backtick-template
3
3
  // https://stackoverflow.com/questions/29182244/convert-a-string-to-a-template-string
4
4
 
5
5
  // Security - Template injection prevention
6
- const { escapeHTML } = require('./MasterSanitizer');
7
- const { logger } = require('./MasterErrorLogger');
6
+ const { escapeHTML } = require('./security/MasterSanitizer');
7
+ const { logger } = require('./error/MasterErrorLogger');
8
8
 
9
9
  var replace = ''.replace;
10
10