voltjs-framework 1.0.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.
- package/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS CORS Handler
|
|
3
|
+
*
|
|
4
|
+
* Cross-Origin Resource Sharing configuration.
|
|
5
|
+
* Secure by default, fully customizable.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
class CORSHandler {
|
|
11
|
+
/** Apply CORS headers to a response */
|
|
12
|
+
static apply(req, res, options = {}) {
|
|
13
|
+
const config = {
|
|
14
|
+
origin: options.origin || '*',
|
|
15
|
+
methods: options.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
16
|
+
allowedHeaders: options.allowedHeaders || ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'],
|
|
17
|
+
exposedHeaders: options.exposedHeaders || [],
|
|
18
|
+
credentials: options.credentials !== undefined ? options.credentials : true,
|
|
19
|
+
maxAge: options.maxAge || 86400,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Handle origin
|
|
23
|
+
const requestOrigin = req.headers.origin;
|
|
24
|
+
|
|
25
|
+
if (typeof config.origin === 'function') {
|
|
26
|
+
const allowed = config.origin(requestOrigin);
|
|
27
|
+
if (allowed) {
|
|
28
|
+
res.setHeader('Access-Control-Allow-Origin', typeof allowed === 'string' ? allowed : requestOrigin);
|
|
29
|
+
}
|
|
30
|
+
} else if (Array.isArray(config.origin)) {
|
|
31
|
+
if (config.origin.includes(requestOrigin)) {
|
|
32
|
+
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
33
|
+
}
|
|
34
|
+
} else if (config.origin === '*' && !config.credentials) {
|
|
35
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
36
|
+
} else if (requestOrigin) {
|
|
37
|
+
if (config.origin === '*' || config.origin === requestOrigin) {
|
|
38
|
+
res.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Methods
|
|
43
|
+
res.setHeader('Access-Control-Allow-Methods', config.methods.join(', '));
|
|
44
|
+
|
|
45
|
+
// Allowed headers
|
|
46
|
+
res.setHeader('Access-Control-Allow-Headers', config.allowedHeaders.join(', '));
|
|
47
|
+
|
|
48
|
+
// Exposed headers
|
|
49
|
+
if (config.exposedHeaders.length > 0) {
|
|
50
|
+
res.setHeader('Access-Control-Expose-Headers', config.exposedHeaders.join(', '));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Credentials
|
|
54
|
+
if (config.credentials) {
|
|
55
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Max age (for preflight cache)
|
|
59
|
+
res.setHeader('Access-Control-Max-Age', String(config.maxAge));
|
|
60
|
+
|
|
61
|
+
// Vary header
|
|
62
|
+
res.setHeader('Vary', 'Origin');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Middleware: CORS */
|
|
66
|
+
static middleware(options = {}) {
|
|
67
|
+
return (req, res) => {
|
|
68
|
+
CORSHandler.apply(req, res, options);
|
|
69
|
+
|
|
70
|
+
// Handle preflight requests
|
|
71
|
+
if (req.method === 'OPTIONS') {
|
|
72
|
+
res.writeHead(204);
|
|
73
|
+
res.end();
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { CORSHandler };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS CSRF Protection
|
|
3
|
+
*
|
|
4
|
+
* Protects against Cross-Site Request Forgery attacks using
|
|
5
|
+
* double-submit cookie pattern and synchronizer tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
class CSRF {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this._secret = options.secret || crypto.randomBytes(32).toString('hex');
|
|
15
|
+
this._tokenLength = options.tokenLength || 32;
|
|
16
|
+
this._cookieName = options.cookieName || '_volt_csrf';
|
|
17
|
+
this._headerName = options.headerName || 'x-csrf-token';
|
|
18
|
+
this._fieldName = options.fieldName || '_csrf';
|
|
19
|
+
this._safeMethods = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
20
|
+
this._excludePaths = new Set(options.exclude || []);
|
|
21
|
+
this._tokens = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Generate a CSRF token */
|
|
25
|
+
generateToken(sessionId = 'default') {
|
|
26
|
+
const token = crypto.randomBytes(this._tokenLength).toString('hex');
|
|
27
|
+
const salt = crypto.randomBytes(8).toString('hex');
|
|
28
|
+
const hash = crypto
|
|
29
|
+
.createHmac('sha256', this._secret)
|
|
30
|
+
.update(`${token}${salt}`)
|
|
31
|
+
.digest('hex');
|
|
32
|
+
|
|
33
|
+
this._tokens.set(token, {
|
|
34
|
+
hash,
|
|
35
|
+
sessionId,
|
|
36
|
+
createdAt: Date.now(),
|
|
37
|
+
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Verify a CSRF token */
|
|
44
|
+
verifyToken(token, sessionId = 'default') {
|
|
45
|
+
const stored = this._tokens.get(token);
|
|
46
|
+
if (!stored) return false;
|
|
47
|
+
if (stored.expiresAt < Date.now()) {
|
|
48
|
+
this._tokens.delete(token);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (stored.sessionId !== sessionId) return false;
|
|
52
|
+
|
|
53
|
+
// Delete after use (one-time use tokens)
|
|
54
|
+
this._tokens.delete(token);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Middleware: CSRF protection */
|
|
59
|
+
middleware(options = {}) {
|
|
60
|
+
return (req, res) => {
|
|
61
|
+
// Skip safe methods
|
|
62
|
+
if (this._safeMethods.has(req.method)) {
|
|
63
|
+
// Attach token generator to response for forms
|
|
64
|
+
req.csrfToken = () => {
|
|
65
|
+
const token = this.generateToken(req.cookies?.[this._cookieName] || 'default');
|
|
66
|
+
res.cookie(this._cookieName, token, {
|
|
67
|
+
httpOnly: true,
|
|
68
|
+
sameSite: 'Strict',
|
|
69
|
+
secure: req.headers['x-forwarded-proto'] === 'https',
|
|
70
|
+
});
|
|
71
|
+
return token;
|
|
72
|
+
};
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Skip excluded paths
|
|
77
|
+
if (this._excludePaths.has(req.path)) return;
|
|
78
|
+
|
|
79
|
+
// Get token from request
|
|
80
|
+
const token =
|
|
81
|
+
req.body?.[this._fieldName] ||
|
|
82
|
+
req.headers[this._headerName] ||
|
|
83
|
+
req.query?.[this._fieldName];
|
|
84
|
+
|
|
85
|
+
if (!token) {
|
|
86
|
+
res.json({ error: 'CSRF token missing' }, 403);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!this.verifyToken(token)) {
|
|
91
|
+
res.json({ error: 'CSRF token invalid' }, 403);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Generate new token for next request
|
|
96
|
+
req.csrfToken = () => this.generateToken();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Cleanup expired tokens periodically */
|
|
101
|
+
cleanup() {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
for (const [token, data] of this._tokens) {
|
|
104
|
+
if (data.expiresAt < now) {
|
|
105
|
+
this._tokens.delete(token);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Start auto-cleanup */
|
|
111
|
+
startAutoCleanup(intervalMs = 300000) {
|
|
112
|
+
this._cleanupInterval = setInterval(() => this.cleanup(), intervalMs);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Stop auto-cleanup */
|
|
117
|
+
stopAutoCleanup() {
|
|
118
|
+
if (this._cleanupInterval) {
|
|
119
|
+
clearInterval(this._cleanupInterval);
|
|
120
|
+
}
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { CSRF };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Encryption Utilities
|
|
3
|
+
*
|
|
4
|
+
* AES-256-GCM encryption, key derivation, and token generation.
|
|
5
|
+
* Uses Node.js native crypto — zero external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
class Encryption {
|
|
13
|
+
constructor(secretKey) {
|
|
14
|
+
this._key = secretKey
|
|
15
|
+
? crypto.scryptSync(secretKey, 'volt-salt', 32)
|
|
16
|
+
: crypto.randomBytes(32);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Encrypt a string using AES-256-GCM */
|
|
20
|
+
encrypt(plaintext) {
|
|
21
|
+
const iv = crypto.randomBytes(16);
|
|
22
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', this._key, iv);
|
|
23
|
+
|
|
24
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
|
25
|
+
encrypted += cipher.final('hex');
|
|
26
|
+
const authTag = cipher.getAuthTag();
|
|
27
|
+
|
|
28
|
+
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Decrypt an AES-256-GCM encrypted string */
|
|
32
|
+
decrypt(ciphertext) {
|
|
33
|
+
try {
|
|
34
|
+
const [ivHex, authTagHex, encrypted] = ciphertext.split(':');
|
|
35
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
36
|
+
const authTag = Buffer.from(authTagHex, 'hex');
|
|
37
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', this._key, iv);
|
|
38
|
+
decipher.setAuthTag(authTag);
|
|
39
|
+
|
|
40
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
41
|
+
decrypted += decipher.final('utf8');
|
|
42
|
+
return decrypted;
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error('Decryption failed — invalid ciphertext or key');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Encrypt an object (serializes to JSON) */
|
|
49
|
+
encryptObject(obj) {
|
|
50
|
+
return this.encrypt(JSON.stringify(obj));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Decrypt to an object */
|
|
54
|
+
decryptObject(ciphertext) {
|
|
55
|
+
return JSON.parse(this.decrypt(ciphertext));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Generate a cryptographically secure random token */
|
|
59
|
+
static generateToken(length = 32) {
|
|
60
|
+
return crypto.randomBytes(length).toString('hex');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Generate a secure random string (URL-safe) */
|
|
64
|
+
static generateUrlSafeToken(length = 32) {
|
|
65
|
+
return crypto.randomBytes(length).toString('base64url');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Generate a numeric OTP */
|
|
69
|
+
static generateOTP(digits = 6) {
|
|
70
|
+
const max = Math.pow(10, digits);
|
|
71
|
+
const otp = crypto.randomInt(0, max);
|
|
72
|
+
return String(otp).padStart(digits, '0');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** SHA-256 hash */
|
|
76
|
+
static sha256(input) {
|
|
77
|
+
return crypto.createHash('sha256').update(input).digest('hex');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** SHA-512 hash */
|
|
81
|
+
static sha512(input) {
|
|
82
|
+
return crypto.createHash('sha512').update(input).digest('hex');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** MD5 hash (not for security — for checksums) */
|
|
86
|
+
static md5(input) {
|
|
87
|
+
return crypto.createHash('md5').update(input).digest('hex');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** HMAC-SHA256 */
|
|
91
|
+
static hmac(input, secret) {
|
|
92
|
+
return crypto.createHmac('sha256', secret).update(input).digest('hex');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Compare two strings in constant time (prevent timing attacks) */
|
|
96
|
+
static timingSafeCompare(a, b) {
|
|
97
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
98
|
+
if (a.length !== b.length) return false;
|
|
99
|
+
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Derive a key from password using scrypt */
|
|
103
|
+
static deriveKey(password, salt, keyLength = 32) {
|
|
104
|
+
const actualSalt = salt || crypto.randomBytes(16).toString('hex');
|
|
105
|
+
const key = crypto.scryptSync(password, actualSalt, keyLength);
|
|
106
|
+
return { key: key.toString('hex'), salt: actualSalt };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { Encryption };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Security Headers (Helmet)
|
|
3
|
+
*
|
|
4
|
+
* Sets secure HTTP headers to protect against common web vulnerabilities.
|
|
5
|
+
* All protections enabled by default.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
class SecurityHeaders {
|
|
11
|
+
/** Apply all security headers */
|
|
12
|
+
static apply(res, options = {}) {
|
|
13
|
+
const headers = {
|
|
14
|
+
// Prevent MIME type sniffing
|
|
15
|
+
'X-Content-Type-Options': 'nosniff',
|
|
16
|
+
|
|
17
|
+
// Prevent clickjacking
|
|
18
|
+
'X-Frame-Options': options.frameOptions || 'DENY',
|
|
19
|
+
|
|
20
|
+
// XSS filter
|
|
21
|
+
'X-XSS-Protection': '1; mode=block',
|
|
22
|
+
|
|
23
|
+
// Referrer policy
|
|
24
|
+
'Referrer-Policy': options.referrerPolicy || 'strict-origin-when-cross-origin',
|
|
25
|
+
|
|
26
|
+
// Strict Transport Security
|
|
27
|
+
'Strict-Transport-Security': options.hsts || 'max-age=31536000; includeSubDomains; preload',
|
|
28
|
+
|
|
29
|
+
// Content Security Policy
|
|
30
|
+
'Content-Security-Policy': options.csp || SecurityHeaders._defaultCSP(options),
|
|
31
|
+
|
|
32
|
+
// Permissions Policy
|
|
33
|
+
'Permissions-Policy': options.permissionsPolicy || SecurityHeaders._defaultPermissions(),
|
|
34
|
+
|
|
35
|
+
// Prevent browsers from caching sensitive pages
|
|
36
|
+
...(options.noCache && {
|
|
37
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
38
|
+
'Pragma': 'no-cache',
|
|
39
|
+
'Expires': '0',
|
|
40
|
+
'Surrogate-Control': 'no-store',
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
// Cross-Origin headers
|
|
44
|
+
'Cross-Origin-Opener-Policy': options.coop || 'same-origin',
|
|
45
|
+
'Cross-Origin-Resource-Policy': options.corp || 'same-origin',
|
|
46
|
+
|
|
47
|
+
// Remove X-Powered-By (optionally re-add as VoltJS)
|
|
48
|
+
'X-Powered-By': options.poweredBy !== false ? 'VoltJS' : undefined,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
52
|
+
if (value !== undefined) {
|
|
53
|
+
res.setHeader(key, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Middleware: Apply security headers */
|
|
59
|
+
static middleware(options = {}) {
|
|
60
|
+
return (req, res) => {
|
|
61
|
+
SecurityHeaders.apply(res, options);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Default Content Security Policy */
|
|
66
|
+
static _defaultCSP(options = {}) {
|
|
67
|
+
const directives = {
|
|
68
|
+
'default-src': ["'self'"],
|
|
69
|
+
'script-src': ["'self'"],
|
|
70
|
+
'style-src': ["'self'", "'unsafe-inline'"],
|
|
71
|
+
'img-src': ["'self'", 'data:', 'https:'],
|
|
72
|
+
'font-src': ["'self'"],
|
|
73
|
+
'connect-src': ["'self'"],
|
|
74
|
+
'media-src': ["'self'"],
|
|
75
|
+
'object-src': ["'none'"],
|
|
76
|
+
'frame-src': ["'none'"],
|
|
77
|
+
'base-uri': ["'self'"],
|
|
78
|
+
'form-action': ["'self'"],
|
|
79
|
+
'frame-ancestors': ["'none'"],
|
|
80
|
+
...(options.cspDirectives || {}),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return Object.entries(directives)
|
|
84
|
+
.map(([key, values]) => `${key} ${values.join(' ')}`)
|
|
85
|
+
.join('; ');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Default Permissions Policy */
|
|
89
|
+
static _defaultPermissions() {
|
|
90
|
+
return [
|
|
91
|
+
'camera=()',
|
|
92
|
+
'microphone=()',
|
|
93
|
+
'geolocation=()',
|
|
94
|
+
'payment=()',
|
|
95
|
+
'usb=()',
|
|
96
|
+
'magnetometer=()',
|
|
97
|
+
'gyroscope=()',
|
|
98
|
+
'accelerometer=()',
|
|
99
|
+
].join(', ');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { SecurityHeaders };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Security Module
|
|
3
|
+
*
|
|
4
|
+
* All-in-one security for your application.
|
|
5
|
+
* Everything is ON by default — secure out of the box.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { Auth } = require('./auth');
|
|
11
|
+
const { CSRF } = require('./csrf');
|
|
12
|
+
const { XSSProtection } = require('./xss');
|
|
13
|
+
const { CORSHandler } = require('./cors');
|
|
14
|
+
const { RateLimiter } = require('./rateLimit');
|
|
15
|
+
const { Encryption } = require('./encryption');
|
|
16
|
+
const { SecurityHeaders } = require('./helmet');
|
|
17
|
+
const { InputSanitizer } = require('./sanitizer');
|
|
18
|
+
|
|
19
|
+
class Security {
|
|
20
|
+
/**
|
|
21
|
+
* Apply all security middleware at once
|
|
22
|
+
*/
|
|
23
|
+
static all(options = {}) {
|
|
24
|
+
return (req, res, next) => {
|
|
25
|
+
// Apply security headers
|
|
26
|
+
SecurityHeaders.apply(res, options.headers);
|
|
27
|
+
|
|
28
|
+
// CORS
|
|
29
|
+
CORSHandler.apply(req, res, options.cors);
|
|
30
|
+
|
|
31
|
+
// Rate limiting
|
|
32
|
+
const rateResult = RateLimiter.check(req, options.rateLimit);
|
|
33
|
+
if (!rateResult.allowed) {
|
|
34
|
+
res.writeHead(429, { 'Content-Type': 'application/json', 'Retry-After': rateResult.retryAfter });
|
|
35
|
+
res.end(JSON.stringify({ error: 'Too many requests', retryAfter: rateResult.retryAfter }));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// XSS sanitize input
|
|
40
|
+
if (req.body && options.xss !== false) {
|
|
41
|
+
req.body = XSSProtection.sanitizeObject(req.body);
|
|
42
|
+
}
|
|
43
|
+
if (req.query && options.xss !== false) {
|
|
44
|
+
req.query = XSSProtection.sanitizeObject(req.query);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Input sanitization
|
|
48
|
+
if (options.sanitize !== false) {
|
|
49
|
+
InputSanitizer.sanitizeRequest(req);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
next();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Quick setup: add all security to a Volt app
|
|
58
|
+
*/
|
|
59
|
+
static secure(app, options = {}) {
|
|
60
|
+
app.use(Security.all(options));
|
|
61
|
+
return app;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
Security,
|
|
67
|
+
Auth,
|
|
68
|
+
CSRF,
|
|
69
|
+
XSSProtection,
|
|
70
|
+
CORSHandler,
|
|
71
|
+
RateLimiter,
|
|
72
|
+
Encryption,
|
|
73
|
+
SecurityHeaders,
|
|
74
|
+
InputSanitizer,
|
|
75
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Protects against brute force and DDoS attacks.
|
|
5
|
+
* Memory-based by default, supports custom stores.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
class RateLimiter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
this._windowMs = options.windowMs || 15 * 60 * 1000; // 15 min
|
|
13
|
+
this._max = options.max || 100;
|
|
14
|
+
this._message = options.message || 'Too many requests, please try again later';
|
|
15
|
+
this._statusCode = options.statusCode || 429;
|
|
16
|
+
this._keyGenerator = options.keyGenerator || ((req) => {
|
|
17
|
+
return req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
18
|
+
});
|
|
19
|
+
this._skipIf = options.skipIf || null;
|
|
20
|
+
this._store = new Map();
|
|
21
|
+
this._cleanup = setInterval(() => this._cleanupExpired(), this._windowMs);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Check if a request is rate limited */
|
|
25
|
+
check(req) {
|
|
26
|
+
const key = this._keyGenerator(req);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
|
|
29
|
+
if (!this._store.has(key)) {
|
|
30
|
+
this._store.set(key, { count: 1, startTime: now, resetTime: now + this._windowMs });
|
|
31
|
+
return { allowed: true, remaining: this._max - 1, resetTime: now + this._windowMs };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const entry = this._store.get(key);
|
|
35
|
+
|
|
36
|
+
// Check if window has expired
|
|
37
|
+
if (now > entry.resetTime) {
|
|
38
|
+
this._store.set(key, { count: 1, startTime: now, resetTime: now + this._windowMs });
|
|
39
|
+
return { allowed: true, remaining: this._max - 1, resetTime: now + this._windowMs };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
entry.count++;
|
|
43
|
+
this._store.set(key, entry);
|
|
44
|
+
|
|
45
|
+
if (entry.count > this._max) {
|
|
46
|
+
return {
|
|
47
|
+
allowed: false,
|
|
48
|
+
remaining: 0,
|
|
49
|
+
resetTime: entry.resetTime,
|
|
50
|
+
retryAfter: Math.ceil((entry.resetTime - now) / 1000),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { allowed: true, remaining: this._max - entry.count, resetTime: entry.resetTime };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Middleware: Rate limiting */
|
|
58
|
+
static middleware(options = {}) {
|
|
59
|
+
const limiter = new RateLimiter(options);
|
|
60
|
+
|
|
61
|
+
return (req, res) => {
|
|
62
|
+
// Skip if condition met
|
|
63
|
+
if (limiter._skipIf && limiter._skipIf(req)) return;
|
|
64
|
+
|
|
65
|
+
const result = limiter.check(req);
|
|
66
|
+
|
|
67
|
+
// Set rate limit headers
|
|
68
|
+
res.setHeader('X-RateLimit-Limit', limiter._max);
|
|
69
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, result.remaining));
|
|
70
|
+
res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000));
|
|
71
|
+
|
|
72
|
+
if (!result.allowed) {
|
|
73
|
+
res.setHeader('Retry-After', result.retryAfter);
|
|
74
|
+
res.json({
|
|
75
|
+
error: limiter._message,
|
|
76
|
+
retryAfter: result.retryAfter,
|
|
77
|
+
}, limiter._statusCode);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Create API-specific rate limiter (stricter) */
|
|
84
|
+
static apiLimiter(options = {}) {
|
|
85
|
+
return RateLimiter.middleware({
|
|
86
|
+
windowMs: options.windowMs || 60 * 1000, // 1 minute
|
|
87
|
+
max: options.max || 30,
|
|
88
|
+
...options,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Create auth-specific rate limiter (very strict) */
|
|
93
|
+
static authLimiter(options = {}) {
|
|
94
|
+
return RateLimiter.middleware({
|
|
95
|
+
windowMs: options.windowMs || 15 * 60 * 1000, // 15 min
|
|
96
|
+
max: options.max || 5,
|
|
97
|
+
message: 'Too many login attempts, please try again later',
|
|
98
|
+
...options,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Clean up expired entries */
|
|
103
|
+
_cleanupExpired() {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
for (const [key, entry] of this._store) {
|
|
106
|
+
if (now > entry.resetTime) {
|
|
107
|
+
this._store.delete(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Destroy the rate limiter (clear interval) */
|
|
113
|
+
destroy() {
|
|
114
|
+
clearInterval(this._cleanup);
|
|
115
|
+
this._store.clear();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { RateLimiter };
|