mastercontroller 1.3.0 → 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 +2 -0
- package/MasterPipeline.js +1 -1
- package/MasterTools.js +40 -13
- package/README.md +171 -35
- package/package.json +1 -1
- package/security/SessionSecurity.js +99 -2
package/MasterControl.js
CHANGED
|
@@ -270,6 +270,7 @@ class MasterControl {
|
|
|
270
270
|
'MasterError',
|
|
271
271
|
'MasterCors',
|
|
272
272
|
'MasterSession',
|
|
273
|
+
'SessionSecurity',
|
|
273
274
|
'MasterSocket',
|
|
274
275
|
'MasterHtml',
|
|
275
276
|
'MasterTemplate',
|
|
@@ -619,6 +620,7 @@ class MasterControl {
|
|
|
619
620
|
'MasterRequest': './MasterRequest',
|
|
620
621
|
'MasterCors': './MasterCors',
|
|
621
622
|
'MasterSession': './MasterSession',
|
|
623
|
+
'SessionSecurity': './security/SessionSecurity',
|
|
622
624
|
'MasterSocket': './MasterSocket',
|
|
623
625
|
'MasterHtml': './MasterHtml',
|
|
624
626
|
'MasterTemplate': './MasterTemplate',
|
package/MasterPipeline.js
CHANGED
|
@@ -44,7 +44,7 @@ class MasterPipeline {
|
|
|
44
44
|
/**
|
|
45
45
|
* Run: Add terminal middleware that ends the pipeline
|
|
46
46
|
*
|
|
47
|
-
* Terminal middleware signature: async (ctx) => {
|
|
47
|
+
* Terminal middleware signature: async (ctx) => { ... send response ... }
|
|
48
48
|
* - Does NOT call next()
|
|
49
49
|
* - Must send response
|
|
50
50
|
*
|
package/MasterTools.js
CHANGED
|
@@ -51,21 +51,48 @@ class MasterTools{
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
encrypt(payload, secret){
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
// Generate random IV (16 bytes for AES)
|
|
55
|
+
const iv = crypto.randomBytes(16);
|
|
56
|
+
|
|
57
|
+
// Create 256-bit key from secret
|
|
58
|
+
const key = crypto.createHash('sha256').update(String(secret)).digest();
|
|
59
|
+
|
|
60
|
+
// Create cipher with AES-256-CBC
|
|
61
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
62
|
+
|
|
63
|
+
// Encrypt payload
|
|
64
|
+
let encrypted = cipher.update(String(payload), 'utf8', 'hex');
|
|
65
|
+
encrypted += cipher.final('hex');
|
|
66
|
+
|
|
67
|
+
// Prepend IV to encrypted data (IV is not secret, needed for decryption)
|
|
68
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
61
69
|
}
|
|
62
|
-
|
|
70
|
+
|
|
63
71
|
decrypt(encryption, secret){
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
72
|
+
try {
|
|
73
|
+
// Split IV and encrypted data
|
|
74
|
+
const parts = encryption.split(':');
|
|
75
|
+
if (parts.length !== 2) {
|
|
76
|
+
throw new Error('Invalid encrypted data format');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
80
|
+
const encryptedData = parts[1];
|
|
81
|
+
|
|
82
|
+
// Create 256-bit key from secret
|
|
83
|
+
const key = crypto.createHash('sha256').update(String(secret)).digest();
|
|
84
|
+
|
|
85
|
+
// Create decipher
|
|
86
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
87
|
+
|
|
88
|
+
// Decrypt
|
|
89
|
+
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
|
90
|
+
decrypted += decipher.final('utf8');
|
|
91
|
+
|
|
92
|
+
return decrypted;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error('Decryption failed: ' + error.message);
|
|
95
|
+
}
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
generateRandomKey(hash){
|
package/README.md
CHANGED
|
@@ -790,94 +790,230 @@ master.cors.init({
|
|
|
790
790
|
|
|
791
791
|
## Sessions
|
|
792
792
|
|
|
793
|
-
|
|
793
|
+
MasterController provides **two session systems**:
|
|
794
|
+
- **`master.session`** (NEW) - Secure, Rails/Django-style sessions with automatic regeneration and protection (RECOMMENDED)
|
|
795
|
+
- **`master.sessions`** (LEGACY) - Original cookie-based session API (backward compatibility only)
|
|
794
796
|
|
|
795
|
-
|
|
797
|
+
### Secure Sessions (NEW - Recommended)
|
|
798
|
+
|
|
799
|
+
#### `master.session.init(options)`
|
|
800
|
+
|
|
801
|
+
Initialize secure sessions with Rails/Django-style `req.session` object (auto-registers with middleware pipeline).
|
|
796
802
|
|
|
797
803
|
```javascript
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
804
|
+
// Environment-specific configuration
|
|
805
|
+
const isProduction = master.environmentType === 'production';
|
|
806
|
+
|
|
807
|
+
master.session.init({
|
|
808
|
+
cookieName: 'mc_session',
|
|
809
|
+
maxAge: isProduction ? 3600000 : 86400000, // Production: 1 hour, Dev: 24 hours
|
|
810
|
+
httpOnly: true, // Prevent JavaScript access (XSS protection)
|
|
811
|
+
secure: isProduction, // HTTPS only in production
|
|
812
|
+
sameSite: isProduction ? 'strict' : 'lax', // CSRF protection
|
|
813
|
+
rolling: true, // Extend session on each request
|
|
814
|
+
regenerateInterval: 900000, // Regenerate session ID every 15 minutes
|
|
815
|
+
useFingerprint: false // Session hijacking detection (opt-in)
|
|
805
816
|
});
|
|
806
817
|
```
|
|
807
818
|
|
|
808
|
-
|
|
819
|
+
**Security Features:**
|
|
820
|
+
- ✅ 32-byte (256-bit) session IDs (cryptographically secure)
|
|
821
|
+
- ✅ Automatic session regeneration (prevents fixation attacks)
|
|
822
|
+
- ✅ HttpOnly cookies (prevents XSS cookie theft)
|
|
823
|
+
- ✅ Secure flag for HTTPS (prevents MITM attacks)
|
|
824
|
+
- ✅ SameSite CSRF protection
|
|
825
|
+
- ✅ Rolling sessions (extends expiry on activity)
|
|
826
|
+
- ✅ Automatic cleanup of expired sessions
|
|
827
|
+
- ✅ Optional fingerprinting (detects hijacking)
|
|
828
|
+
|
|
829
|
+
#### Using Sessions in Controllers
|
|
809
830
|
|
|
810
|
-
|
|
811
|
-
Create a session.
|
|
831
|
+
Sessions are accessed via `obj.request.session` object:
|
|
812
832
|
|
|
813
833
|
```javascript
|
|
814
834
|
class AuthController {
|
|
815
835
|
login(obj) {
|
|
816
836
|
const user = authenticateUser(obj.params.formData);
|
|
817
837
|
|
|
818
|
-
|
|
838
|
+
// Set session data (Rails/Express style)
|
|
839
|
+
obj.request.session.userId = user.id;
|
|
840
|
+
obj.request.session.username = user.name;
|
|
841
|
+
obj.request.session.loggedInAt = Date.now();
|
|
819
842
|
|
|
820
843
|
this.redirect('/dashboard');
|
|
821
844
|
}
|
|
845
|
+
|
|
846
|
+
logout(obj) {
|
|
847
|
+
// Destroy entire session
|
|
848
|
+
master.session.destroy(obj.request, obj.response);
|
|
849
|
+
this.redirect('/');
|
|
850
|
+
}
|
|
822
851
|
}
|
|
823
852
|
```
|
|
824
853
|
|
|
825
|
-
#### `master.sessions.get(name, request, secret)`
|
|
826
|
-
Retrieve session data.
|
|
827
|
-
|
|
828
854
|
```javascript
|
|
829
855
|
class DashboardController {
|
|
830
856
|
index(obj) {
|
|
831
|
-
|
|
857
|
+
// Read session data
|
|
858
|
+
const userId = obj.request.session.userId;
|
|
832
859
|
|
|
833
|
-
if (!
|
|
860
|
+
if (!userId) {
|
|
834
861
|
this.redirect('/login');
|
|
835
862
|
return;
|
|
836
863
|
}
|
|
837
864
|
|
|
838
|
-
this.render('dashboard', {
|
|
865
|
+
this.render('dashboard', { userId });
|
|
839
866
|
}
|
|
840
867
|
}
|
|
841
868
|
```
|
|
842
869
|
|
|
843
|
-
####
|
|
844
|
-
|
|
870
|
+
#### Session Management API
|
|
871
|
+
|
|
872
|
+
**`master.session.destroy(req, res)`** - Destroy session completely
|
|
845
873
|
|
|
846
874
|
```javascript
|
|
847
|
-
|
|
848
|
-
logout(obj) {
|
|
849
|
-
master.sessions.delete('user', obj.response);
|
|
850
|
-
this.redirect('/');
|
|
851
|
-
}
|
|
852
|
-
}
|
|
875
|
+
master.session.destroy(obj.request, obj.response);
|
|
853
876
|
```
|
|
854
877
|
|
|
855
|
-
|
|
856
|
-
Clear all sessions (useful for testing).
|
|
878
|
+
**`master.session.touch(sessionId)`** - Extend session expiry
|
|
857
879
|
|
|
858
880
|
```javascript
|
|
859
|
-
master.
|
|
881
|
+
master.session.touch(obj.request.sessionId);
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**`master.session.getSessionCount()`** - Get active session count (monitoring)
|
|
885
|
+
|
|
886
|
+
```javascript
|
|
887
|
+
const count = master.session.getSessionCount();
|
|
888
|
+
console.log(`Active sessions: ${count}`);
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**`master.session.clearAllSessions()`** - Clear all sessions (testing only)
|
|
892
|
+
|
|
893
|
+
```javascript
|
|
894
|
+
master.session.clearAllSessions();
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
#### Environment-Specific Best Practices
|
|
898
|
+
|
|
899
|
+
```javascript
|
|
900
|
+
// Get recommended settings
|
|
901
|
+
const settings = master.session.getBestPractices('production');
|
|
902
|
+
master.session.init(settings);
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
**Production Settings:**
|
|
906
|
+
- Secure: true (HTTPS only)
|
|
907
|
+
- SameSite: 'strict' (maximum CSRF protection)
|
|
908
|
+
- MaxAge: 1 hour (short-lived sessions)
|
|
909
|
+
- RegenerateInterval: 15 minutes
|
|
910
|
+
|
|
911
|
+
**Development Settings:**
|
|
912
|
+
- Secure: false (allow HTTP)
|
|
913
|
+
- SameSite: 'lax' (easier testing)
|
|
914
|
+
- MaxAge: 24 hours (convenient for development)
|
|
915
|
+
- RegenerateInterval: 1 hour
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
### Legacy Sessions (Backward Compatibility)
|
|
920
|
+
|
|
921
|
+
**⚠️ DEPRECATED: Use `master.session` (singular) for new projects.**
|
|
922
|
+
|
|
923
|
+
The original `master.sessions` (plural) API is maintained for backward compatibility but lacks modern security features.
|
|
924
|
+
|
|
925
|
+
#### `master.sessions.init(options)`
|
|
926
|
+
|
|
927
|
+
Initialize legacy sessions (auto-registers with middleware pipeline).
|
|
928
|
+
|
|
929
|
+
```javascript
|
|
930
|
+
master.sessions.init({
|
|
931
|
+
secret: 'your-secret-key',
|
|
932
|
+
maxAge: 900000, // 15 minutes
|
|
933
|
+
httpOnly: true,
|
|
934
|
+
secure: true, // HTTPS only
|
|
935
|
+
sameSite: 'strict', // Must be string: 'strict', 'lax', or 'none'
|
|
936
|
+
path: '/'
|
|
937
|
+
});
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
#### Legacy Session API
|
|
941
|
+
|
|
942
|
+
**`master.sessions.set(name, data, response, secret, options)`** - Create a session
|
|
943
|
+
|
|
944
|
+
```javascript
|
|
945
|
+
master.sessions.set('user', userData, obj.response);
|
|
860
946
|
```
|
|
861
947
|
|
|
862
|
-
|
|
948
|
+
**`master.sessions.get(name, request, secret)`** - Retrieve session data
|
|
863
949
|
|
|
864
|
-
|
|
950
|
+
```javascript
|
|
951
|
+
const user = master.sessions.get('user', obj.request);
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
**`master.sessions.delete(name, response)`** - Delete a session
|
|
865
955
|
|
|
866
|
-
|
|
956
|
+
```javascript
|
|
957
|
+
master.sessions.delete('user', obj.response);
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**`master.sessions.reset()`** - Clear all sessions
|
|
961
|
+
|
|
962
|
+
```javascript
|
|
963
|
+
master.sessions.reset();
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
#### Legacy Cookie Methods
|
|
967
|
+
|
|
968
|
+
**`master.sessions.setCookie(name, value, response, options)`**
|
|
867
969
|
```javascript
|
|
868
970
|
master.sessions.setCookie('theme', 'dark', obj.response);
|
|
869
971
|
```
|
|
870
972
|
|
|
871
|
-
|
|
973
|
+
**`master.sessions.getCookie(name, request, secret)`**
|
|
872
974
|
```javascript
|
|
873
975
|
const theme = master.sessions.getCookie('theme', obj.request);
|
|
874
976
|
```
|
|
875
977
|
|
|
876
|
-
|
|
978
|
+
**`master.sessions.deleteCookie(name, response, options)`**
|
|
877
979
|
```javascript
|
|
878
980
|
master.sessions.deleteCookie('theme', obj.response);
|
|
879
981
|
```
|
|
880
982
|
|
|
983
|
+
#### Migration Guide: Legacy → Secure Sessions
|
|
984
|
+
|
|
985
|
+
**Old (master.sessions):**
|
|
986
|
+
```javascript
|
|
987
|
+
// Set
|
|
988
|
+
master.sessions.set('user', userData, obj.response);
|
|
989
|
+
|
|
990
|
+
// Get
|
|
991
|
+
const user = master.sessions.get('user', obj.request);
|
|
992
|
+
|
|
993
|
+
// Delete
|
|
994
|
+
master.sessions.delete('user', obj.response);
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
**New (master.session):**
|
|
998
|
+
```javascript
|
|
999
|
+
// Set (Rails/Express style)
|
|
1000
|
+
obj.request.session.user = userData;
|
|
1001
|
+
|
|
1002
|
+
// Get
|
|
1003
|
+
const user = obj.request.session.user;
|
|
1004
|
+
|
|
1005
|
+
// Delete
|
|
1006
|
+
master.session.destroy(obj.request, obj.response);
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Benefits of migration:**
|
|
1010
|
+
- ✅ Automatic session regeneration (prevents fixation)
|
|
1011
|
+
- ✅ 32-byte session IDs (stronger than 20-byte)
|
|
1012
|
+
- ✅ Rolling sessions (better UX)
|
|
1013
|
+
- ✅ Automatic cleanup (no memory leaks)
|
|
1014
|
+
- ✅ Rails/Express-style API (more familiar)
|
|
1015
|
+
- ✅ No broken encryption (legacy has crypto bugs)
|
|
1016
|
+
|
|
881
1017
|
---
|
|
882
1018
|
|
|
883
1019
|
## Security
|
package/package.json
CHANGED
|
@@ -25,8 +25,8 @@ class SessionSecurity {
|
|
|
25
25
|
this.domain = options.domain || null;
|
|
26
26
|
this.path = options.path || '/';
|
|
27
27
|
|
|
28
|
-
// Session fingerprinting
|
|
29
|
-
this.useFingerprint = options.useFingerprint
|
|
28
|
+
// Session fingerprinting (disabled by default like ASP.NET Core)
|
|
29
|
+
this.useFingerprint = options.useFingerprint === true;
|
|
30
30
|
|
|
31
31
|
// Start cleanup interval
|
|
32
32
|
this._startCleanup();
|
|
@@ -407,6 +407,103 @@ const SESSION_BEST_PRACTICES = {
|
|
|
407
407
|
}
|
|
408
408
|
};
|
|
409
409
|
|
|
410
|
+
// MasterController Integration
|
|
411
|
+
const master = require('../MasterControl');
|
|
412
|
+
|
|
413
|
+
// Create MasterController-compatible wrapper
|
|
414
|
+
class MasterSessionSecurity {
|
|
415
|
+
constructor() {
|
|
416
|
+
this._instance = null;
|
|
417
|
+
this._options = {};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Initialize session security (Rails/Django style)
|
|
422
|
+
* Auto-registers with middleware pipeline
|
|
423
|
+
*/
|
|
424
|
+
init(options = {}) {
|
|
425
|
+
this._options = options;
|
|
426
|
+
this._instance = new SessionSecurity(options);
|
|
427
|
+
|
|
428
|
+
// Auto-register with pipeline if available
|
|
429
|
+
if (master.pipeline) {
|
|
430
|
+
master.pipeline.use(this._instance.middleware());
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get middleware function
|
|
438
|
+
*/
|
|
439
|
+
middleware() {
|
|
440
|
+
if (!this._instance) {
|
|
441
|
+
this.init();
|
|
442
|
+
}
|
|
443
|
+
return this._instance.middleware();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Destroy session
|
|
448
|
+
*/
|
|
449
|
+
destroy(req, res) {
|
|
450
|
+
if (!this._instance) {
|
|
451
|
+
throw new Error('SessionSecurity not initialized. Call master.session.init() first.');
|
|
452
|
+
}
|
|
453
|
+
return this._instance.destroySession(req, res);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get session by ID
|
|
458
|
+
*/
|
|
459
|
+
getSession(sessionId) {
|
|
460
|
+
if (!this._instance) {
|
|
461
|
+
throw new Error('SessionSecurity not initialized. Call master.session.init() first.');
|
|
462
|
+
}
|
|
463
|
+
return this._instance.getSession(sessionId);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Touch session (extend expiry)
|
|
468
|
+
*/
|
|
469
|
+
touch(sessionId) {
|
|
470
|
+
if (!this._instance) {
|
|
471
|
+
throw new Error('SessionSecurity not initialized. Call master.session.init() first.');
|
|
472
|
+
}
|
|
473
|
+
return this._instance.touch(sessionId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Get session count (monitoring)
|
|
478
|
+
*/
|
|
479
|
+
getSessionCount() {
|
|
480
|
+
if (!this._instance) {
|
|
481
|
+
throw new Error('SessionSecurity not initialized. Call master.session.init() first.');
|
|
482
|
+
}
|
|
483
|
+
return this._instance.getSessionCount();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Clear all sessions (testing only)
|
|
488
|
+
*/
|
|
489
|
+
clearAllSessions() {
|
|
490
|
+
if (!this._instance) {
|
|
491
|
+
throw new Error('SessionSecurity not initialized. Call master.session.init() first.');
|
|
492
|
+
}
|
|
493
|
+
return this._instance.clearAllSessions();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get recommended settings for environment
|
|
498
|
+
*/
|
|
499
|
+
getBestPractices(env) {
|
|
500
|
+
return SESSION_BEST_PRACTICES[env] || SESSION_BEST_PRACTICES.development;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Auto-register with MasterController
|
|
505
|
+
master.extend("session", MasterSessionSecurity);
|
|
506
|
+
|
|
410
507
|
module.exports = {
|
|
411
508
|
SessionSecurity,
|
|
412
509
|
session,
|