mastercontroller 1.2.14 → 1.3.1
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 +2 -1
- package/MasterControl.js +150 -101
- package/MasterCors.js +29 -0
- package/MasterPipeline.js +344 -0
- package/MasterRouter.js +44 -22
- package/MasterSession.js +19 -0
- package/MasterTimeout.js +332 -0
- package/MasterTools.js +40 -13
- package/README.md +1632 -36
- package/docs/timeout-and-error-handling.md +712 -0
- package/error/MasterErrorRenderer.js +529 -0
- package/package.json +5 -5
- package/security/SecurityMiddleware.js +73 -1
- package/security/SessionSecurity.js +99 -2
package/MasterControl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// MasterControl - by Alexander rich
|
|
2
|
-
// version 1.0.
|
|
2
|
+
// version 1.0.252
|
|
3
3
|
|
|
4
4
|
var url = require('url');
|
|
5
5
|
var fileserver = require('fs');
|
|
@@ -192,7 +192,16 @@ class MasterControl {
|
|
|
192
192
|
|
|
193
193
|
component(folderLocation, innerFolder){
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// Enhanced: Support both relative (to master.root) and absolute paths
|
|
196
|
+
// If folderLocation is absolute, use it directly; otherwise join with master.root
|
|
197
|
+
var rootFolderLocation;
|
|
198
|
+
if (path.isAbsolute(folderLocation)) {
|
|
199
|
+
// Absolute path provided - use it directly
|
|
200
|
+
rootFolderLocation = path.join(folderLocation, innerFolder);
|
|
201
|
+
} else {
|
|
202
|
+
// Relative path - join with master.root (original behavior)
|
|
203
|
+
rootFolderLocation = path.join(this.root, folderLocation, innerFolder);
|
|
204
|
+
}
|
|
196
205
|
|
|
197
206
|
// Structure is always: {rootFolderLocation}/config/initializers/config.js
|
|
198
207
|
var configPath = path.join(rootFolderLocation, 'config', 'initializers', 'config.js');
|
|
@@ -251,6 +260,9 @@ class MasterControl {
|
|
|
251
260
|
// before user config initializes them.
|
|
252
261
|
try {
|
|
253
262
|
$that.addInternalTools([
|
|
263
|
+
'MasterPipeline',
|
|
264
|
+
'MasterTimeout',
|
|
265
|
+
'MasterErrorRenderer',
|
|
254
266
|
'MasterAction',
|
|
255
267
|
'MasterActionFilters',
|
|
256
268
|
'MasterRouter',
|
|
@@ -258,6 +270,7 @@ class MasterControl {
|
|
|
258
270
|
'MasterError',
|
|
259
271
|
'MasterCors',
|
|
260
272
|
'MasterSession',
|
|
273
|
+
'SessionSecurity',
|
|
261
274
|
'MasterSocket',
|
|
262
275
|
'MasterHtml',
|
|
263
276
|
'MasterTemplate',
|
|
@@ -267,6 +280,10 @@ class MasterControl {
|
|
|
267
280
|
} catch (e) {
|
|
268
281
|
console.error('[MasterControl] Failed to load internal tools:', e && e.message);
|
|
269
282
|
}
|
|
283
|
+
|
|
284
|
+
// Register core middleware that must run for framework to function
|
|
285
|
+
$that._registerCoreMiddleware();
|
|
286
|
+
|
|
270
287
|
if(type === "http"){
|
|
271
288
|
$that.serverProtocol = "http";
|
|
272
289
|
return http.createServer(async function(req, res) {
|
|
@@ -423,120 +440,148 @@ class MasterControl {
|
|
|
423
440
|
});
|
|
424
441
|
}
|
|
425
442
|
|
|
426
|
-
|
|
443
|
+
/**
|
|
444
|
+
* Register core middleware that must run for the framework to function
|
|
445
|
+
* This includes: static files, body parsing, scoped services, routing, error handling
|
|
446
|
+
*/
|
|
447
|
+
_registerCoreMiddleware(){
|
|
427
448
|
var $that = this;
|
|
428
|
-
console.log("path", `${req.method} ${req.url}`);
|
|
429
449
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
});
|
|
450
|
+
// 1. Static File Serving
|
|
451
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
452
|
+
if (ctx.isStatic) {
|
|
453
|
+
// Serve static files
|
|
454
|
+
let pathname = `.${ctx.request.url}`;
|
|
455
|
+
|
|
456
|
+
fs.exists(pathname, function (exist) {
|
|
457
|
+
if (!exist) {
|
|
458
|
+
ctx.response.statusCode = 404;
|
|
459
|
+
ctx.response.end(`File ${pathname} not found!`);
|
|
460
|
+
return;
|
|
442
461
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
res.setHeader('access-control-allow-methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
447
|
-
if (req.headers['access-control-request-headers']) {
|
|
448
|
-
res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']);
|
|
462
|
+
|
|
463
|
+
if (fs.statSync(pathname).isDirectory()) {
|
|
464
|
+
pathname += '/index' + path.parse(pathname).ext;
|
|
449
465
|
}
|
|
450
|
-
res.setHeader('access-control-max-age', '86400');
|
|
451
|
-
}
|
|
452
|
-
} catch (e) {
|
|
453
|
-
res.setHeader('access-control-allow-origin', '*');
|
|
454
|
-
res.setHeader('access-control-allow-methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
455
|
-
if (req.headers['access-control-request-headers']) {
|
|
456
|
-
res.setHeader('access-control-allow-headers', req.headers['access-control-request-headers']);
|
|
457
|
-
}
|
|
458
|
-
res.setHeader('access-control-max-age', '86400');
|
|
459
|
-
}
|
|
460
|
-
res.statusCode = 204;
|
|
461
|
-
res.setHeader('content-length', '0');
|
|
462
|
-
res.end();
|
|
463
|
-
return;
|
|
464
|
-
}
|
|
465
466
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
467
|
+
fs.readFile(pathname, function(err, data) {
|
|
468
|
+
if (err) {
|
|
469
|
+
ctx.response.statusCode = 500;
|
|
470
|
+
ctx.response.end(`Error getting the file: ${err}.`);
|
|
471
|
+
} else {
|
|
472
|
+
const mimeType = $that.router.findMimeType(path.parse(pathname).ext);
|
|
473
|
+
ctx.response.setHeader('Content-type', mimeType || 'text/plain');
|
|
474
|
+
ctx.response.end(data);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
});
|
|
474
478
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
// extract URL path
|
|
478
|
-
let pathname = `.${parsedUrl.pathname}`;
|
|
479
|
+
return; // Terminal - don't call next()
|
|
480
|
+
}
|
|
479
481
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
+
await next(); // Not static, continue pipeline
|
|
483
|
+
});
|
|
482
484
|
|
|
483
|
-
//
|
|
485
|
+
// 2. Timeout Tracking (optional - disabled by default until init)
|
|
486
|
+
// Will be configured by user in config.js with master.timeout.init()
|
|
487
|
+
// This is just a placeholder registration - actual timeout is set in user config
|
|
484
488
|
|
|
489
|
+
// 3. Request Body Parsing (always needed)
|
|
490
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
491
|
+
// Parse body using MasterRequest
|
|
492
|
+
const params = await $that.request.getRequestParam(ctx.request, ctx.response);
|
|
485
493
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if(requestObject !== -1){
|
|
490
|
-
// HSTS header if enabled
|
|
491
|
-
if(this.serverProtocol === 'https' && this._hstsEnabled){
|
|
492
|
-
res.setHeader('strict-transport-security', `max-age=${this._hstsMaxAge}; includeSubDomains`);
|
|
494
|
+
// Merge parsed params into context
|
|
495
|
+
if (params && params.query) {
|
|
496
|
+
ctx.params.query = params.query;
|
|
493
497
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
loadedDone = $that._loadedFunc(requestObject);
|
|
497
|
-
if (loadedDone){
|
|
498
|
-
require(`${this.root}/config/load`)(requestObject);
|
|
499
|
-
}
|
|
498
|
+
if (params && params.formData) {
|
|
499
|
+
ctx.params.formData = params.formData;
|
|
500
500
|
}
|
|
501
|
-
|
|
502
|
-
|
|
501
|
+
|
|
502
|
+
await next();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// 4. Load Scoped Services (per request - always needed)
|
|
506
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
507
|
+
for (var key in $that._scopedList) {
|
|
508
|
+
var className = $that._scopedList[key];
|
|
509
|
+
$that.requestList[key] = new className();
|
|
503
510
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// if the file is found, set Content-type and send data
|
|
531
|
-
res.setHeader('Content-type', mimeType || 'text/plain' );
|
|
532
|
-
res.end(data);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
|
|
511
|
+
await next();
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// 4. HSTS Header (if enabled for HTTPS)
|
|
515
|
+
$that.pipeline.use(async (ctx, next) => {
|
|
516
|
+
if ($that.serverProtocol === 'https' && $that._hstsEnabled) {
|
|
517
|
+
ctx.response.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
518
|
+
}
|
|
519
|
+
await next();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// 5. Routing (TERMINAL - always needed)
|
|
523
|
+
$that.pipeline.run(async (ctx) => {
|
|
524
|
+
// Load config/load which triggers routing
|
|
525
|
+
require(`${$that.root}/config/load`)(ctx);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// 6. Global Error Handler
|
|
529
|
+
$that.pipeline.useError(async (error, ctx, next) => {
|
|
530
|
+
logger.error({
|
|
531
|
+
code: 'MC_ERR_PIPELINE',
|
|
532
|
+
message: 'Error in middleware pipeline',
|
|
533
|
+
error: error.message,
|
|
534
|
+
stack: error.stack,
|
|
535
|
+
path: ctx.request.url,
|
|
536
|
+
method: ctx.type
|
|
536
537
|
});
|
|
538
|
+
|
|
539
|
+
if (!ctx.response.headersSent) {
|
|
540
|
+
ctx.response.statusCode = 500;
|
|
541
|
+
ctx.response.setHeader('Content-Type', 'application/json');
|
|
542
|
+
ctx.response.end(JSON.stringify({
|
|
543
|
+
error: 'Internal Server Error',
|
|
544
|
+
message: process.env.NODE_ENV === 'production'
|
|
545
|
+
? 'An error occurred'
|
|
546
|
+
: error.message
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async serverRun(req, res){
|
|
553
|
+
var $that = this;
|
|
554
|
+
console.log("path", `${req.method} ${req.url}`);
|
|
555
|
+
|
|
556
|
+
// Create request context for middleware pipeline
|
|
557
|
+
const parsedUrl = url.parse(req.url);
|
|
558
|
+
const pathname = parsedUrl.pathname;
|
|
559
|
+
const ext = path.parse(pathname).ext;
|
|
560
|
+
|
|
561
|
+
const context = {
|
|
562
|
+
request: req,
|
|
563
|
+
response: res,
|
|
564
|
+
requrl: url.parse(req.url, true),
|
|
565
|
+
pathName: pathname.replace(/^\/|\/$/g, '').toLowerCase(),
|
|
566
|
+
type: req.method.toLowerCase(),
|
|
567
|
+
params: {},
|
|
568
|
+
state: {}, // User-defined state shared across middleware
|
|
569
|
+
master: $that, // Access to framework instance
|
|
570
|
+
isStatic: ext !== '' // Is this a static file request?
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// Execute middleware pipeline
|
|
574
|
+
try {
|
|
575
|
+
await $that.pipeline.execute(context);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error('Pipeline execution failed:', error);
|
|
578
|
+
if (!res.headersSent) {
|
|
579
|
+
res.statusCode = 500;
|
|
580
|
+
res.end('Internal Server Error');
|
|
581
|
+
}
|
|
537
582
|
}
|
|
538
|
-
|
|
539
|
-
} // end
|
|
583
|
+
|
|
584
|
+
} // end serverRun()
|
|
540
585
|
|
|
541
586
|
start(server){
|
|
542
587
|
this.server = server;
|
|
@@ -565,6 +610,9 @@ class MasterControl {
|
|
|
565
610
|
if(requiredList.constructor === Array){
|
|
566
611
|
// Map module names to their new organized paths
|
|
567
612
|
const modulePathMap = {
|
|
613
|
+
'MasterPipeline': './MasterPipeline',
|
|
614
|
+
'MasterTimeout': './MasterTimeout',
|
|
615
|
+
'MasterErrorRenderer': './error/MasterErrorRenderer',
|
|
568
616
|
'MasterError': './error/MasterError',
|
|
569
617
|
'MasterAction': './MasterAction',
|
|
570
618
|
'MasterActionFilters': './MasterActionFilters',
|
|
@@ -572,6 +620,7 @@ class MasterControl {
|
|
|
572
620
|
'MasterRequest': './MasterRequest',
|
|
573
621
|
'MasterCors': './MasterCors',
|
|
574
622
|
'MasterSession': './MasterSession',
|
|
623
|
+
'SessionSecurity': './security/SessionSecurity',
|
|
575
624
|
'MasterSocket': './MasterSocket',
|
|
576
625
|
'MasterHtml': './MasterHtml',
|
|
577
626
|
'MasterTemplate': './MasterTemplate',
|
package/MasterCors.js
CHANGED
|
@@ -11,6 +11,13 @@ class MasterCors{
|
|
|
11
11
|
else{
|
|
12
12
|
master.error.log("cors options missing", "warn");
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
// Auto-register with pipeline if available
|
|
16
|
+
if (master.pipeline) {
|
|
17
|
+
master.pipeline.use(this.middleware());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return this; // Chainable
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
load(params){
|
|
@@ -167,6 +174,28 @@ class MasterCors{
|
|
|
167
174
|
}
|
|
168
175
|
}
|
|
169
176
|
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get CORS middleware for the pipeline
|
|
180
|
+
* Handles both preflight OPTIONS requests and regular requests
|
|
181
|
+
*/
|
|
182
|
+
middleware() {
|
|
183
|
+
var $that = this;
|
|
184
|
+
|
|
185
|
+
return async (ctx, next) => {
|
|
186
|
+
// Handle preflight OPTIONS request
|
|
187
|
+
if (ctx.type === 'options') {
|
|
188
|
+
$that.load({ request: ctx.request, response: ctx.response });
|
|
189
|
+
ctx.response.statusCode = 204;
|
|
190
|
+
ctx.response.end();
|
|
191
|
+
return; // Terminal - don't call next()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Regular request - apply CORS headers
|
|
195
|
+
$that.load({ request: ctx.request, response: ctx.response });
|
|
196
|
+
await next();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
170
199
|
}
|
|
171
200
|
|
|
172
201
|
master.extend("cors", MasterCors);
|