thyscript 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.
Files changed (49) hide show
  1. package/bin/thy-node.js +332 -0
  2. package/bin/thy.js +828 -0
  3. package/bin/thys.js +25 -0
  4. package/examples/calculator.thy +274 -0
  5. package/examples/calculator.thy.js +628 -0
  6. package/examples/hello.thy +34 -0
  7. package/examples/login.thy +382 -0
  8. package/examples/login.thy.js +710 -0
  9. package/package.json +54 -0
  10. package/scripts/_class.js +18 -0
  11. package/scripts/_class2.js +11 -0
  12. package/scripts/_class3.js +21 -0
  13. package/scripts/_dstruct.js +20 -0
  14. package/scripts/_dstruct2.js +24 -0
  15. package/scripts/_scaffold.js +16 -0
  16. package/scripts/_scaffold2.js +16 -0
  17. package/scripts/_scaffold3.js +26 -0
  18. package/scripts/_tpl_compile.js +13 -0
  19. package/scripts/_tpl_test.js +9 -0
  20. package/scripts/_typeof.js +26 -0
  21. package/scripts/_typeof2.js +25 -0
  22. package/scripts/patch_genmethod.js +44 -0
  23. package/scripts/smoke_test.js +167 -0
  24. package/scripts/smoke_test2.js +151 -0
  25. package/scripts/test_control_panel.js +58 -0
  26. package/src/compiler/CodeGenerator.js +1449 -0
  27. package/src/compiler/JITCompiler.js +325 -0
  28. package/src/database/DatabaseEngine.js +931 -0
  29. package/src/errors/ErrorDetector.js +305 -0
  30. package/src/errors/ErrorMessages.js +497 -0
  31. package/src/index.js +169 -0
  32. package/src/lexer/Lexer.js +342 -0
  33. package/src/lexer/TokenTypes.js +1234 -0
  34. package/src/memory/MemoryManager.js +283 -0
  35. package/src/parser/AST.js +606 -0
  36. package/src/parser/Parser.js +1942 -0
  37. package/src/runtime/ThyRuntime.js +265 -0
  38. package/src/runtime/stdlib/ArrayLib.js +207 -0
  39. package/src/runtime/stdlib/FileLib.js +168 -0
  40. package/src/runtime/stdlib/MathLib.js +411 -0
  41. package/src/runtime/stdlib/StringLib.js +387 -0
  42. package/src/server/ControlPanel.js +1188 -0
  43. package/src/server/HTMLCSSEngine.js +866 -0
  44. package/src/server/SecurityEngine.js +714 -0
  45. package/src/server/ServerEngine.js +738 -0
  46. package/tests/test1_math.thy +82 -0
  47. package/tests/test2_oop.thy +133 -0
  48. package/tests/test3_server.thy +143 -0
  49. package/tests/test4_functions.thy +159 -0
@@ -0,0 +1,714 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ // Optional dependencies with graceful fallback implementations using native crypto
6
+ let bcrypt, jwt;
7
+ try {
8
+ bcrypt = require('bcrypt');
9
+ } catch (e) {
10
+ bcrypt = {
11
+ hash: async (password, saltRounds = 10) => {
12
+ const salt = crypto.randomBytes(16).toString('hex');
13
+ const hash = crypto.scryptSync(password, salt, 64).toString('hex');
14
+ return `pbkdf2$${salt}$${hash}`;
15
+ },
16
+ compare: async (password, storedHash) => {
17
+ if (!storedHash) return false;
18
+ if (storedHash.startsWith('pbkdf2$')) {
19
+ const parts = storedHash.split('$');
20
+ const salt = parts[1];
21
+ const hash = parts[2];
22
+ const attempt = crypto.scryptSync(password, salt, 64).toString('hex');
23
+ return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(attempt));
24
+ }
25
+ return false;
26
+ },
27
+ };
28
+ }
29
+ try {
30
+ jwt = require('jsonwebtoken');
31
+ } catch (e) {
32
+ jwt = {
33
+ sign: (payload, secret, options = {}) => {
34
+ const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
35
+ const body = Buffer.from(JSON.stringify({ ...payload, exp: options.expiresIn ? Math.floor(Date.now() / 1000) + 86400 : undefined })).toString('base64url');
36
+ const sig = crypto.createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
37
+ return `${header}.${body}.${sig}`;
38
+ },
39
+ verify: (token, secret) => {
40
+ const parts = token.split('.');
41
+ if (parts.length !== 3) throw new Error('Invalid token structure');
42
+ const expectedSig = crypto.createHmac('sha256', secret).update(`${parts[0]}.${parts[1]}`).digest('base64url');
43
+ if (parts[2] !== expectedSig) throw new Error('Invalid signature');
44
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
45
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) throw new Error('Token expired');
46
+ return payload;
47
+ },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * ThyScript SecurityEngine
53
+ * Comprehensive security utilities for ThyScript servers.
54
+ */
55
+ class SecurityEngine {
56
+ constructor() {
57
+ // CSRF token store: token -> { createdAt, used }
58
+ this._csrfStore = new Map();
59
+ this._csrfTTL = 3600000; // 1 hour
60
+
61
+ // Rate limit store: key -> { count, resetAt }
62
+ this._rateLimitStore = new Map();
63
+
64
+ // Blocked IPs
65
+ this._blockedIPs = new Set();
66
+
67
+ // Honeypot log
68
+ this._honeypotLog = [];
69
+
70
+ // XSS patterns
71
+ this._xssPatterns = [
72
+ { pattern: /<script[\s\S]*?>[\s\S]*?<\/script>/gi, name: 'script-tag' },
73
+ { pattern: /javascript\s*:/gi, name: 'js-protocol' },
74
+ { pattern: /on\w+\s*=\s*["'][^"']*["']/gi, name: 'inline-event' },
75
+ { pattern: /<iframe[\s\S]*?>/gi, name: 'iframe' },
76
+ { pattern: /<object[\s\S]*?>/gi, name: 'object' },
77
+ { pattern: /<embed[\s\S]*?>/gi, name: 'embed' },
78
+ { pattern: /eval\s*\(/gi, name: 'eval' },
79
+ { pattern: /expression\s*\(/gi, name: 'css-expression' },
80
+ { pattern: /vbscript\s*:/gi, name: 'vbscript' },
81
+ { pattern: /<link[\s\S]*?>/gi, name: 'link-tag' },
82
+ ];
83
+
84
+ // SQL injection patterns
85
+ this._sqlPatterns = [
86
+ /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|UNION|TRUNCATE|CAST|CONVERT|DECLARE|FETCH|CURSOR|KILL|GRANT|REVOKE)\b)/gi,
87
+ /('|")\s*;\s*--/gi,
88
+ /\/\*[\s\S]*?\*\//g,
89
+ /\bOR\b\s+\d+\s*=\s*\d+/gi,
90
+ /\bAND\b\s+\d+\s*=\s*\d+/gi,
91
+ /\b(OR|AND)\b\s+['"]?\w+['"]?\s*=\s*['"]?\w+['"]?/gi,
92
+ /;\s*(DROP|DELETE|INSERT|UPDATE|CREATE)\s/gi,
93
+ /'\s*--/g,
94
+ /xp_cmdshell/gi,
95
+ /information_schema/gi,
96
+ /sys\.(tables|columns|objects)/gi,
97
+ ];
98
+
99
+ // Code injection patterns
100
+ this._codeInjectionPatterns = [
101
+ /\$\{[\s\S]*?\}/g, // Template literals
102
+ /`[\s\S]*?`/g, // Backtick strings
103
+ /eval\s*\(/gi,
104
+ /Function\s*\(/gi,
105
+ /new\s+Function/gi,
106
+ /setTimeout\s*\(/gi,
107
+ /setInterval\s*\(/gi,
108
+ /require\s*\(/gi,
109
+ /child_process/gi,
110
+ /process\.exit/gi,
111
+ /os\.system/gi,
112
+ /__proto__/gi,
113
+ /constructor\s*\[/gi,
114
+ /prototype\s*\[/gi,
115
+ ];
116
+ }
117
+
118
+ // ─────────────────────────────────────────────
119
+ // Password Hashing
120
+ // ─────────────────────────────────────────────
121
+
122
+ /**
123
+ * Hashes a password using bcrypt.
124
+ * @param {string} password
125
+ * @param {number} [saltRounds=12]
126
+ * @returns {Promise<string>}
127
+ */
128
+ async hashPassword(password, saltRounds = 12) {
129
+ if (!password || typeof password !== 'string') {
130
+ throw new Error('Password must be a non-empty string');
131
+ }
132
+ return bcrypt.hash(password, saltRounds);
133
+ }
134
+
135
+ /**
136
+ * Verifies a password against a bcrypt hash.
137
+ * @param {string} password
138
+ * @param {string} hash
139
+ * @returns {Promise<boolean>}
140
+ */
141
+ async verifyPassword(password, hash) {
142
+ if (!password || !hash) return false;
143
+ return bcrypt.compare(password, hash);
144
+ }
145
+
146
+ // ─────────────────────────────────────────────
147
+ // JWT
148
+ // ─────────────────────────────────────────────
149
+
150
+ /**
151
+ * Generates a signed JWT.
152
+ * @param {object} payload
153
+ * @param {string} secret
154
+ * @param {object} [options] - jsonwebtoken sign options (e.g. { expiresIn: '1h' })
155
+ * @returns {string}
156
+ */
157
+ generateToken(payload, secret, options = { expiresIn: '24h' }) {
158
+ if (!payload || !secret) throw new Error('payload and secret are required');
159
+ return jwt.sign(payload, secret, options);
160
+ }
161
+
162
+ /**
163
+ * Verifies and decodes a JWT.
164
+ * @param {string} token
165
+ * @param {string} secret
166
+ * @returns {{ valid: boolean, payload: object|null, error: string|null }}
167
+ */
168
+ verifyToken(token, secret) {
169
+ try {
170
+ const payload = jwt.verify(token, secret);
171
+ return { valid: true, payload, error: null };
172
+ } catch (err) {
173
+ return { valid: false, payload: null, error: err.message };
174
+ }
175
+ }
176
+
177
+ // ─────────────────────────────────────────────
178
+ // AES-256-GCM Encryption
179
+ // ─────────────────────────────────────────────
180
+
181
+ /**
182
+ * Encrypts data with AES-256-GCM.
183
+ * @param {string|Buffer} data
184
+ * @param {string|Buffer} key - 32-byte key (hex string or Buffer)
185
+ * @returns {{ iv: string, tag: string, ciphertext: string }}
186
+ */
187
+ encrypt(data, key) {
188
+ const keyBuf = this._normalizeKey(key);
189
+ const iv = crypto.randomBytes(16);
190
+ const cipher = crypto.createCipheriv('aes-256-gcm', keyBuf, iv);
191
+ const dataStr = typeof data === 'object' ? JSON.stringify(data) : String(data);
192
+ const encrypted = Buffer.concat([cipher.update(dataStr, 'utf8'), cipher.final()]);
193
+ const tag = cipher.getAuthTag();
194
+ return {
195
+ iv: iv.toString('hex'),
196
+ tag: tag.toString('hex'),
197
+ ciphertext: encrypted.toString('hex'),
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Decrypts AES-256-GCM encrypted data.
203
+ * @param {{ iv: string, tag: string, ciphertext: string }} encrypted
204
+ * @param {string|Buffer} key
205
+ * @returns {string}
206
+ */
207
+ decrypt(encrypted, key) {
208
+ const keyBuf = this._normalizeKey(key);
209
+ const iv = Buffer.from(encrypted.iv, 'hex');
210
+ const tag = Buffer.from(encrypted.tag, 'hex');
211
+ const ciphertext = Buffer.from(encrypted.ciphertext, 'hex');
212
+ const decipher = crypto.createDecipheriv('aes-256-gcm', keyBuf, iv);
213
+ decipher.setAuthTag(tag);
214
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
215
+ return decrypted.toString('utf8');
216
+ }
217
+
218
+ _normalizeKey(key) {
219
+ if (Buffer.isBuffer(key)) {
220
+ if (key.length !== 32) throw new Error('AES-256-GCM requires a 32-byte key');
221
+ return key;
222
+ }
223
+ if (typeof key === 'string') {
224
+ // Treat as hex string if 64 chars, otherwise derive via SHA-256
225
+ if (key.length === 64) return Buffer.from(key, 'hex');
226
+ return crypto.createHash('sha256').update(key).digest();
227
+ }
228
+ throw new Error('Key must be a Buffer or string');
229
+ }
230
+
231
+ // ─────────────────────────────────────────────
232
+ // Hashing
233
+ // ─────────────────────────────────────────────
234
+
235
+ /**
236
+ * Returns SHA-256 hash of data as hex string.
237
+ * @param {string|Buffer} data
238
+ * @returns {string}
239
+ */
240
+ hashSHA256(data) {
241
+ return crypto.createHash('sha256').update(data).digest('hex');
242
+ }
243
+
244
+ /**
245
+ * Returns MD5 hash of data as hex string.
246
+ * @param {string|Buffer} data
247
+ * @returns {string}
248
+ */
249
+ hashMD5(data) {
250
+ return crypto.createHash('md5').update(data).digest('hex');
251
+ }
252
+
253
+ /**
254
+ * Returns HMAC-SHA256 signature.
255
+ * @param {string} data
256
+ * @param {string} key
257
+ * @returns {string}
258
+ */
259
+ hmacSHA256(data, key) {
260
+ return crypto.createHmac('sha256', key).update(data).digest('hex');
261
+ }
262
+
263
+ // ─────────────────────────────────────────────
264
+ // Key & UUID Generation
265
+ // ─────────────────────────────────────────────
266
+
267
+ /**
268
+ * Generates a cryptographically secure random key.
269
+ * @param {number} [length=32] - Length in bytes.
270
+ * @param {'hex'|'base64'|'buffer'} [encoding='hex']
271
+ * @returns {string|Buffer}
272
+ */
273
+ generateKey(length = 32, encoding = 'hex') {
274
+ const buf = crypto.randomBytes(length);
275
+ if (encoding === 'buffer') return buf;
276
+ return buf.toString(encoding);
277
+ }
278
+
279
+ /**
280
+ * Generates a UUID v4.
281
+ * @returns {string}
282
+ */
283
+ generateUUID() {
284
+ return crypto.randomUUID
285
+ ? crypto.randomUUID()
286
+ : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
287
+ const r = (crypto.randomBytes(1)[0] & 0xff) % 16;
288
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
289
+ return v.toString(16);
290
+ });
291
+ }
292
+
293
+ // ─────────────────────────────────────────────
294
+ // Input Sanitization & Detection
295
+ // ─────────────────────────────────────────────
296
+
297
+ /**
298
+ * Sanitizes a string to prevent XSS.
299
+ * @param {string} input
300
+ * @returns {string}
301
+ */
302
+ sanitizeInput(input) {
303
+ if (typeof input !== 'string') return input;
304
+ let result = input;
305
+ // Remove dangerous patterns
306
+ for (const { pattern } of this._xssPatterns) {
307
+ result = result.replace(pattern, '');
308
+ }
309
+ // HTML entity encode
310
+ result = result
311
+ .replace(/&/g, '&amp;')
312
+ .replace(/</g, '&lt;')
313
+ .replace(/>/g, '&gt;')
314
+ .replace(/"/g, '&quot;')
315
+ .replace(/'/g, '&#x27;')
316
+ .replace(/\//g, '&#x2F;');
317
+ return result;
318
+ }
319
+
320
+ /**
321
+ * Detects SQL injection patterns in input.
322
+ * @param {string} input
323
+ * @returns {{ detected: boolean, patterns: string[] }}
324
+ */
325
+ detectSQLInjection(input) {
326
+ if (typeof input !== 'string') return { detected: false, patterns: [] };
327
+ const found = [];
328
+ for (const pattern of this._sqlPatterns) {
329
+ pattern.lastIndex = 0;
330
+ if (pattern.test(input)) found.push(pattern.toString());
331
+ }
332
+ return { detected: found.length > 0, patterns: found };
333
+ }
334
+
335
+ /**
336
+ * Detects code injection patterns in input.
337
+ * @param {string} input
338
+ * @returns {{ detected: boolean, patterns: string[] }}
339
+ */
340
+ detectCodeInjection(input) {
341
+ if (typeof input !== 'string') return { detected: false, patterns: [] };
342
+ const found = [];
343
+ for (const pattern of this._codeInjectionPatterns) {
344
+ pattern.lastIndex = 0;
345
+ if (pattern.test(input)) found.push(pattern.toString());
346
+ }
347
+ return { detected: found.length > 0, patterns: found };
348
+ }
349
+
350
+ // ─────────────────────────────────────────────
351
+ // Validation
352
+ // ─────────────────────────────────────────────
353
+
354
+ /**
355
+ * Validates an email address.
356
+ * @param {string} email
357
+ * @returns {boolean}
358
+ */
359
+ validateEmail(email) {
360
+ if (typeof email !== 'string') return false;
361
+ const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$/;
362
+ return emailRegex.test(email.trim()) && email.length <= 254;
363
+ }
364
+
365
+ /**
366
+ * Validates a URL.
367
+ * @param {string} url
368
+ * @param {string[]} [allowedProtocols=['http:', 'https:']]
369
+ * @returns {boolean}
370
+ */
371
+ validateURL(url, allowedProtocols = ['http:', 'https:']) {
372
+ if (typeof url !== 'string') return false;
373
+ try {
374
+ const parsed = new URL(url);
375
+ return allowedProtocols.includes(parsed.protocol);
376
+ } catch {
377
+ return false;
378
+ }
379
+ }
380
+
381
+ // ─────────────────────────────────────────────
382
+ // Rate Limiting
383
+ // ─────────────────────────────────────────────
384
+
385
+ /**
386
+ * Checks and enforces rate limiting.
387
+ * @param {string} key - Identifier (e.g. IP, user ID).
388
+ * @param {number} maxRequests - Max requests per window.
389
+ * @param {number} windowMs - Window in milliseconds.
390
+ * @returns {{ allowed: boolean, remaining: number, resetAt: number }}
391
+ */
392
+ rateLimit(key, maxRequests, windowMs) {
393
+ const now = Date.now();
394
+ const entry = this._rateLimitStore.get(key);
395
+
396
+ if (!entry || now > entry.resetAt) {
397
+ const newEntry = { count: 1, resetAt: now + windowMs };
398
+ this._rateLimitStore.set(key, newEntry);
399
+ return { allowed: true, remaining: maxRequests - 1, resetAt: newEntry.resetAt };
400
+ }
401
+
402
+ if (entry.count >= maxRequests) {
403
+ return { allowed: false, remaining: 0, resetAt: entry.resetAt };
404
+ }
405
+
406
+ entry.count++;
407
+ return { allowed: true, remaining: maxRequests - entry.count, resetAt: entry.resetAt };
408
+ }
409
+
410
+ /**
411
+ * Returns an Express middleware that enforces rate limiting.
412
+ * @param {number} maxRequests
413
+ * @param {number} windowMs
414
+ * @param {function} [keyFn] - function(req) -> key. Defaults to IP.
415
+ */
416
+ rateLimitMiddleware(maxRequests, windowMs, keyFn) {
417
+ return (req, res, next) => {
418
+ const key = keyFn ? keyFn(req) : (req.ip || req.connection.remoteAddress);
419
+ const result = this.rateLimit(key, maxRequests, windowMs);
420
+ res.setHeader('X-RateLimit-Limit', maxRequests);
421
+ res.setHeader('X-RateLimit-Remaining', result.remaining);
422
+ res.setHeader('X-RateLimit-Reset', Math.ceil(result.resetAt / 1000));
423
+ if (!result.allowed) {
424
+ res.status(429).json({
425
+ error: 'Too Many Requests',
426
+ retryAfter: Math.ceil((result.resetAt - Date.now()) / 1000),
427
+ });
428
+ return;
429
+ }
430
+ next();
431
+ };
432
+ }
433
+
434
+ // ─────────────────────────────────────────────
435
+ // CSRF
436
+ // ─────────────────────────────────────────────
437
+
438
+ /**
439
+ * Creates a CSRF token and stores it.
440
+ * @returns {string} CSRF token.
441
+ */
442
+ createCSRFToken() {
443
+ const token = this.generateKey(32, 'hex');
444
+ this._csrfStore.set(token, { createdAt: Date.now(), used: false });
445
+ // Clean up expired tokens periodically
446
+ if (this._csrfStore.size % 100 === 0) this._cleanCSRFStore();
447
+ return token;
448
+ }
449
+
450
+ /**
451
+ * Verifies a CSRF token (single-use).
452
+ * @param {string} token
453
+ * @returns {boolean}
454
+ */
455
+ verifyCSRFToken(token) {
456
+ const entry = this._csrfStore.get(token);
457
+ if (!entry) return false;
458
+ if (entry.used) return false;
459
+ if (Date.now() - entry.createdAt > this._csrfTTL) {
460
+ this._csrfStore.delete(token);
461
+ return false;
462
+ }
463
+ entry.used = true;
464
+ return true;
465
+ }
466
+
467
+ _cleanCSRFStore() {
468
+ const now = Date.now();
469
+ for (const [token, entry] of this._csrfStore) {
470
+ if (entry.used || now - entry.createdAt > this._csrfTTL) {
471
+ this._csrfStore.delete(token);
472
+ }
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Returns an Express middleware that validates CSRF tokens.
478
+ * Expects token in header 'X-CSRF-Token' or body field '_csrf'.
479
+ * @param {string[]} [skipMethods=['GET','HEAD','OPTIONS']]
480
+ */
481
+ csrfMiddleware(skipMethods = ['GET', 'HEAD', 'OPTIONS']) {
482
+ return (req, res, next) => {
483
+ if (skipMethods.includes(req.method.toUpperCase())) return next();
484
+ const token = req.headers['x-csrf-token'] || (req.body && req.body._csrf);
485
+ if (!token || !this.verifyCSRFToken(token)) {
486
+ res.status(403).json({ error: 'Forbidden', message: 'Invalid or missing CSRF token.' });
487
+ return;
488
+ }
489
+ next();
490
+ };
491
+ }
492
+
493
+ // ─────────────────────────────────────────────
494
+ // IP Blocking
495
+ // ─────────────────────────────────────────────
496
+
497
+ /**
498
+ * Blocks an IP address.
499
+ * @param {string} ip
500
+ * @returns {SecurityEngine}
501
+ */
502
+ blockIP(ip) {
503
+ this._blockedIPs.add(ip);
504
+ return this;
505
+ }
506
+
507
+ /**
508
+ * Unblocks an IP address.
509
+ * @param {string} ip
510
+ * @returns {SecurityEngine}
511
+ */
512
+ unblockIP(ip) {
513
+ this._blockedIPs.delete(ip);
514
+ return this;
515
+ }
516
+
517
+ /**
518
+ * Checks if an IP is blocked.
519
+ * @param {string} ip
520
+ * @returns {boolean}
521
+ */
522
+ isBlockedIP(ip) {
523
+ return this._blockedIPs.has(ip);
524
+ }
525
+
526
+ /**
527
+ * Returns an Express middleware that blocks listed IPs.
528
+ */
529
+ blockIPMiddleware() {
530
+ return (req, res, next) => {
531
+ const ip = req.ip || req.connection.remoteAddress || '';
532
+ if (this.isBlockedIP(ip)) {
533
+ res.status(403).json({ error: 'Forbidden', message: 'Access denied.' });
534
+ return;
535
+ }
536
+ next();
537
+ };
538
+ }
539
+
540
+ // ─────────────────────────────────────────────
541
+ // Honeypot
542
+ // ─────────────────────────────────────────────
543
+
544
+ /**
545
+ * Returns Express middleware that redirects injection attempts to a fake handler.
546
+ * @param {function} fakeServerHandler - Express handler (req, res) for suspicious requests.
547
+ * @param {object} [options]
548
+ * @param {boolean} [options.detectSQL=true]
549
+ * @param {boolean} [options.detectCode=true]
550
+ * @param {boolean} [options.detectXSS=true]
551
+ * @returns {function}
552
+ */
553
+ honeypot(fakeServerHandler, options = { detectSQL: true, detectCode: true, detectXSS: true }) {
554
+ return (req, res, next) => {
555
+ const combined = [
556
+ JSON.stringify(req.query),
557
+ JSON.stringify(req.body || {}),
558
+ req.url,
559
+ ].join(' ');
560
+
561
+ let suspicious = false;
562
+
563
+ if (options.detectSQL !== false) {
564
+ const { detected } = this.detectSQLInjection(combined);
565
+ if (detected) suspicious = true;
566
+ }
567
+
568
+ if (!suspicious && options.detectCode !== false) {
569
+ const { detected } = this.detectCodeInjection(combined);
570
+ if (detected) suspicious = true;
571
+ }
572
+
573
+ if (!suspicious && options.detectXSS !== false) {
574
+ const xssFound = this._xssPatterns.some(({ pattern }) => {
575
+ pattern.lastIndex = 0;
576
+ return pattern.test(combined);
577
+ });
578
+ if (xssFound) suspicious = true;
579
+ }
580
+
581
+ if (suspicious) {
582
+ const ip = req.ip || req.connection.remoteAddress;
583
+ const logEntry = {
584
+ timestamp: new Date().toISOString(),
585
+ ip,
586
+ method: req.method,
587
+ url: req.url,
588
+ query: req.query,
589
+ body: req.body,
590
+ };
591
+ this._honeypotLog.push(logEntry);
592
+ console.warn('[SecurityEngine] Honeypot triggered:', logEntry);
593
+ this.blockIP(ip);
594
+ if (typeof fakeServerHandler === 'function') {
595
+ return fakeServerHandler(req, res);
596
+ }
597
+ return res.status(200).send('OK'); // Fake success
598
+ }
599
+
600
+ next();
601
+ };
602
+ }
603
+
604
+ /**
605
+ * Returns the honeypot activity log.
606
+ * @returns {object[]}
607
+ */
608
+ getHoneypotLog() {
609
+ return [...this._honeypotLog];
610
+ }
611
+
612
+ // ─────────────────────────────────────────────
613
+ // Content Security Policy
614
+ // ─────────────────────────────────────────────
615
+
616
+ /**
617
+ * Returns an Express middleware that sets CSP headers.
618
+ * @param {object} [options] - CSP directives.
619
+ * @returns {function}
620
+ */
621
+ contentSecurityPolicy(options = {}) {
622
+ const directives = Object.assign({
623
+ 'default-src': ["'self'"],
624
+ 'script-src': ["'self'"],
625
+ 'style-src': ["'self'", "'unsafe-inline'"],
626
+ 'img-src': ["'self'", 'data:', 'https:'],
627
+ 'font-src': ["'self'"],
628
+ 'connect-src': ["'self'"],
629
+ 'media-src': ["'self'"],
630
+ 'object-src': ["'none'"],
631
+ 'frame-ancestors': ["'none'"],
632
+ 'base-uri': ["'self'"],
633
+ 'form-action': ["'self'"],
634
+ }, options);
635
+
636
+ const cspString = Object.entries(directives)
637
+ .map(([key, val]) => `${key} ${Array.isArray(val) ? val.join(' ') : val}`)
638
+ .join('; ');
639
+
640
+ return (req, res, next) => {
641
+ res.setHeader('Content-Security-Policy', cspString);
642
+ next();
643
+ };
644
+ }
645
+
646
+ // ─────────────────────────────────────────────
647
+ // Helmet-like Security Headers
648
+ // ─────────────────────────────────────────────
649
+
650
+ /**
651
+ * Returns an Express middleware that sets comprehensive security headers.
652
+ * @param {object} [options]
653
+ * @returns {function}
654
+ */
655
+ helmet(options = {}) {
656
+ return (req, res, next) => {
657
+ // Prevent MIME sniffing
658
+ res.setHeader('X-Content-Type-Options', 'nosniff');
659
+ // Clickjacking protection
660
+ res.setHeader('X-Frame-Options', options.frameOptions || 'DENY');
661
+ // XSS filter (legacy browsers)
662
+ res.setHeader('X-XSS-Protection', '1; mode=block');
663
+ // HSTS
664
+ if (options.hsts !== false) {
665
+ res.setHeader(
666
+ 'Strict-Transport-Security',
667
+ options.hstsHeader || 'max-age=31536000; includeSubDomains; preload'
668
+ );
669
+ }
670
+ // Referrer policy
671
+ res.setHeader('Referrer-Policy', options.referrerPolicy || 'strict-origin-when-cross-origin');
672
+ // Permissions policy
673
+ res.setHeader(
674
+ 'Permissions-Policy',
675
+ options.permissionsPolicy || 'geolocation=(), microphone=(), camera=()'
676
+ );
677
+ // Remove X-Powered-By
678
+ res.removeHeader('X-Powered-By');
679
+ // DNS prefetch control
680
+ res.setHeader('X-DNS-Prefetch-Control', 'off');
681
+ // Expect-CT
682
+ if (options.expectCT) {
683
+ res.setHeader('Expect-CT', 'max-age=86400, enforce');
684
+ }
685
+ // Cache control for sensitive routes
686
+ if (options.noCache) {
687
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
688
+ res.setHeader('Pragma', 'no-cache');
689
+ res.setHeader('Expires', '0');
690
+ }
691
+ next();
692
+ };
693
+ }
694
+
695
+ // ─────────────────────────────────────────────
696
+ // Constant-time comparison (prevent timing attacks)
697
+ // ─────────────────────────────────────────────
698
+
699
+ /**
700
+ * Compares two strings in constant time to prevent timing attacks.
701
+ * @param {string} a
702
+ * @param {string} b
703
+ * @returns {boolean}
704
+ */
705
+ constantTimeEqual(a, b) {
706
+ if (typeof a !== 'string' || typeof b !== 'string') return false;
707
+ const bufA = Buffer.from(a);
708
+ const bufB = Buffer.from(b);
709
+ if (bufA.length !== bufB.length) return false;
710
+ return crypto.timingSafeEqual(bufA, bufB);
711
+ }
712
+ }
713
+
714
+ module.exports = SecurityEngine;