vako 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.
@@ -0,0 +1,1000 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const validator = require('validator');
4
+ const crypto = require('crypto');
5
+
6
+ class RouteManager {
7
+ constructor(app, options) {
8
+ this.app = app;
9
+ this.options = options;
10
+ this.routeMap = new Map();
11
+ this.dynamicRoutes = new Map();
12
+
13
+ // Limite le nombre de routes dynamiques pour éviter les attaques
14
+ this.maxDynamicRoutes = options.maxDynamicRoutes || 1000;
15
+ this.rateLimitCache = new Map();
16
+ this.allowedMethods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'];
17
+
18
+ // Cache pour optimisation
19
+ this.methodsCache = new Map();
20
+ this.pathValidationCache = new Map();
21
+
22
+ // Configuration de sécurité
23
+ this.securityConfig = {
24
+ maxParamLength: options.maxParamLength || 1000,
25
+ maxPathLength: options.maxPathLength || 500,
26
+ rateLimitWindow: options.rateLimitWindow || 60000,
27
+ rateLimitMax: options.rateLimitMax || 100,
28
+ enableSecurityHeaders: options.enableSecurityHeaders !== false,
29
+ contentSecurityPolicy: options.contentSecurityPolicy || "default-src 'self'"
30
+ };
31
+
32
+ // Démarrer le nettoyage périodique
33
+ this.startCacheCleanup();
34
+ }
35
+
36
+ // Validation sécurisée et optimisée des paramètres
37
+ validateRouteInput(method, path, handler) {
38
+ // Cache key pour éviter les revalidations
39
+ const cacheKey = `${method}:${path}:${typeof handler}`;
40
+ if (this.pathValidationCache.has(cacheKey)) {
41
+ const cached = this.pathValidationCache.get(cacheKey);
42
+ return { ...cached, handler }; // Handler peut changer
43
+ }
44
+
45
+ // Validation de la méthode HTTP
46
+ if (!method || typeof method !== 'string') {
47
+ throw new Error('Méthode HTTP invalide');
48
+ }
49
+
50
+ const normalizedMethod = method.toLowerCase();
51
+ if (!this.allowedMethods.includes(normalizedMethod)) {
52
+ throw new Error(`Méthode HTTP non autorisée: ${method}`);
53
+ }
54
+
55
+ // Validation du chemin
56
+ if (!path || typeof path !== 'string') {
57
+ throw new Error('Chemin de route invalide');
58
+ }
59
+
60
+ // Validation contre les attaques par traversée de chemin et injections
61
+ if (this.containsDangerousPatterns(path)) {
62
+ throw new Error('Chemin de route contient des caractères dangereux');
63
+ }
64
+
65
+ // Limite la longueur du chemin
66
+ if (path.length > this.securityConfig.maxPathLength) {
67
+ throw new Error(`Chemin de route trop long (max: ${this.securityConfig.maxPathLength})`);
68
+ }
69
+
70
+ // Validation du handler
71
+ if (!handler || (typeof handler !== 'function' && !Array.isArray(handler))) {
72
+ throw new Error('Handler de route invalide');
73
+ }
74
+
75
+ if (Array.isArray(handler)) {
76
+ handler.forEach((h, index) => {
77
+ if (typeof h !== 'function') {
78
+ throw new Error(`Handler ${index} n'est pas une fonction`);
79
+ }
80
+ });
81
+ }
82
+
83
+ const result = { method: normalizedMethod, path: this.sanitizePath(path) };
84
+
85
+ // Cache le résultat (sans le handler)
86
+ this.pathValidationCache.set(cacheKey, result);
87
+
88
+ return { ...result, handler };
89
+ }
90
+
91
+ // Détection optimisée de patterns dangereux
92
+ containsDangerousPatterns(path) {
93
+ const dangerousPatterns = [
94
+ /\.\./, // Path traversal
95
+ /[\\]/, // Backslashes
96
+ /[<>\"'`]/, // HTML/JS injection
97
+ /javascript:/i, // JavaScript protocol
98
+ /data:/i, // Data URLs
99
+ /vbscript:/i, // VBScript
100
+ /on\w+=/i, // Event handlers
101
+ /eval\s*\(/i, // eval calls
102
+ /expression\s*\(/i, // CSS expressions
103
+ /url\s*\(/i // CSS URLs
104
+ ];
105
+
106
+ return dangerousPatterns.some(pattern => pattern.test(path));
107
+ }
108
+
109
+ // Nettoyage sécurisé et optimisé du chemin
110
+ sanitizePath(routePath) {
111
+ // Normalise et nettoie le chemin
112
+ let cleaned = routePath.trim();
113
+
114
+ // Supprime les caractères de contrôle
115
+ cleaned = cleaned.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
116
+
117
+ // Décode les entités HTML/URL
118
+ try {
119
+ cleaned = decodeURIComponent(cleaned);
120
+ } catch (e) {
121
+ // Si le décodage échoue, utiliser la chaîne originale
122
+ }
123
+
124
+ // Assure que le chemin commence par /
125
+ if (!cleaned.startsWith('/')) {
126
+ cleaned = '/' + cleaned;
127
+ }
128
+
129
+ // Normalise les slashes multiples
130
+ cleaned = cleaned.replace(/\/+/g, '/');
131
+
132
+ // Retire les trailing slashes sauf pour la racine
133
+ if (cleaned.length > 1 && cleaned.endsWith('/')) {
134
+ cleaned = cleaned.slice(0, -1);
135
+ }
136
+
137
+ return cleaned;
138
+ }
139
+
140
+ // Rate limiting amélioré avec sliding window
141
+ checkRateLimit(clientId = 'default', customLimits = {}) {
142
+ const now = Date.now();
143
+ const windowMs = customLimits.window || this.securityConfig.rateLimitWindow;
144
+ const maxRequests = customLimits.max || this.securityConfig.rateLimitMax;
145
+
146
+ if (!this.rateLimitCache.has(clientId)) {
147
+ this.rateLimitCache.set(clientId, {
148
+ count: 1,
149
+ resetTime: now + windowMs,
150
+ requests: [now]
151
+ });
152
+ return true;
153
+ }
154
+
155
+ const limit = this.rateLimitCache.get(clientId);
156
+
157
+ // Nettoyage des anciennes requêtes (sliding window)
158
+ limit.requests = limit.requests.filter(time => time > now - windowMs);
159
+
160
+ if (limit.requests.length >= maxRequests) {
161
+ return false;
162
+ }
163
+
164
+ limit.requests.push(now);
165
+ limit.count = limit.requests.length;
166
+
167
+ return true;
168
+ }
169
+
170
+ // Méthode async corrigée
171
+ async createRoute(method, path, handler, options = {}) {
172
+ try {
173
+ // Validation sécurisée des entrées
174
+ const validated = this.validateRouteInput(method, path, handler);
175
+ method = validated.method;
176
+ path = validated.path;
177
+ handler = validated.handler;
178
+
179
+ // Vérifie les limites de création de routes
180
+ if (this.dynamicRoutes.size >= this.maxDynamicRoutes) {
181
+ throw new Error('Limite de routes dynamiques atteinte');
182
+ }
183
+
184
+ // Rate limiting pour la création de routes
185
+ const clientId = options.clientId || 'route-creation';
186
+ if (!this.checkRateLimit(clientId)) {
187
+ throw new Error('Trop de tentatives de création de routes');
188
+ }
189
+
190
+ // Hook de sécurité avant création
191
+ if (this.app.plugins) {
192
+ await this.app.plugins.executeHook('route:security-check', method, path, handler, options);
193
+ }
194
+
195
+ if (this.routeExists(method, path)) {
196
+ this.app.logger.log('warning', 'Route already exists', `${method.toUpperCase()} ${path}`);
197
+ return this.app;
198
+ }
199
+
200
+ // Wrapper sécurisé pour le handler
201
+ const secureHandler = this.createSecureHandler(handler, method, path, options);
202
+
203
+ if (Array.isArray(secureHandler)) {
204
+ this.app.app[method](path, ...secureHandler);
205
+ } else {
206
+ this.app.app[method](path, secureHandler);
207
+ }
208
+
209
+ const routeKey = `${method}:${path}`;
210
+ this.dynamicRoutes.set(routeKey, {
211
+ method,
212
+ path,
213
+ handler: secureHandler,
214
+ options: this.sanitizeOptions(options),
215
+ createdAt: new Date().toISOString(),
216
+ createdBy: options.createdBy || 'system',
217
+ routeId: this.generateRouteId(method, path)
218
+ });
219
+
220
+ this.app.logger.log('create', 'Route created dynamically', `${method.toUpperCase()} ${path}`);
221
+
222
+ if (this.app.plugins) {
223
+ await this.app.plugins.executeHook('route:created', method, path, secureHandler, options);
224
+ }
225
+
226
+ if (this.app.options.isDev && this.app.devServer) {
227
+ this.app.devServer.broadcast({
228
+ type: 'route-created',
229
+ method: method.toUpperCase(),
230
+ path,
231
+ timestamp: new Date().toISOString()
232
+ });
233
+ }
234
+
235
+ return this.app;
236
+ } catch (error) {
237
+ this.app.logger.log('error', 'Error creating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
238
+ throw error;
239
+ }
240
+ }
241
+
242
+ // Crée un wrapper sécurisé amélioré pour les handlers
243
+ createSecureHandler(handler, method, path, options = {}) {
244
+ const wrapHandler = (originalHandler) => {
245
+ return async (req, res, next) => {
246
+ const startTime = Date.now();
247
+
248
+ try {
249
+ // Headers de sécurité configurables
250
+ if (this.securityConfig.enableSecurityHeaders) {
251
+ this.setSecurityHeaders(res);
252
+ }
253
+
254
+ // Validation des paramètres d'entrée
255
+ this.validateRequestInput(req);
256
+
257
+ // Rate limiting par route si configuré
258
+ if (options.rateLimit) {
259
+ const clientKey = `${req.ip}:${method}:${path}`;
260
+ if (!this.checkRateLimit(clientKey, options.rateLimit)) {
261
+ return res.status(429).json({ error: 'Trop de requêtes' });
262
+ }
263
+ }
264
+
265
+ // Log de sécurité avec plus de détails
266
+ this.app.logger.log('security', 'Route accessed',
267
+ `${method.toUpperCase()} ${path} from ${req.ip} (${req.get('User-Agent') || 'Unknown'})`
268
+ );
269
+
270
+ // Exécute le handler original avec timeout
271
+ const timeoutMs = options.timeout || 30000;
272
+ const handlerPromise = Promise.resolve(originalHandler(req, res, next));
273
+ const timeoutPromise = new Promise((_, reject) =>
274
+ setTimeout(() => reject(new Error('Handler timeout')), timeoutMs)
275
+ );
276
+
277
+ await Promise.race([handlerPromise, timeoutPromise]);
278
+
279
+ // Log de performance
280
+ const duration = Date.now() - startTime;
281
+ if (duration > 1000) {
282
+ this.app.logger.log('performance', 'Slow route',
283
+ `${method.toUpperCase()} ${path} took ${duration}ms`
284
+ );
285
+ }
286
+
287
+ } catch (error) {
288
+ const duration = Date.now() - startTime;
289
+
290
+ this.app.logger.log('error', 'Handler error',
291
+ `${method.toUpperCase()} ${path} → ${error.message} (${duration}ms)`
292
+ );
293
+
294
+ // Réponse d'erreur sécurisée
295
+ if (!res.headersSent) {
296
+ this.sendSecureErrorResponse(res, error);
297
+ }
298
+ }
299
+ };
300
+ };
301
+
302
+ if (Array.isArray(handler)) {
303
+ return handler.map(h => wrapHandler(h));
304
+ } else {
305
+ return wrapHandler(handler);
306
+ }
307
+ }
308
+
309
+ // Headers de sécurité configurables
310
+ setSecurityHeaders(res) {
311
+ const headers = {
312
+ 'X-Content-Type-Options': 'nosniff',
313
+ 'X-Frame-Options': 'DENY',
314
+ 'X-XSS-Protection': '1; mode=block',
315
+ 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
316
+ 'Referrer-Policy': 'strict-origin-when-cross-origin',
317
+ 'Content-Security-Policy': this.securityConfig.contentSecurityPolicy
318
+ };
319
+
320
+ Object.entries(headers).forEach(([name, value]) => {
321
+ if (!res.getHeader(name)) {
322
+ res.setHeader(name, value);
323
+ }
324
+ });
325
+ }
326
+
327
+ // Réponse d'erreur sécurisée
328
+ sendSecureErrorResponse(res, error) {
329
+ const isDev = process.env.NODE_ENV !== 'production';
330
+ const errorId = this.generateErrorId();
331
+
332
+ // Log l'erreur avec un ID unique
333
+ this.app.logger.log('error', 'Request error', `ID: ${errorId} - ${error.message}`);
334
+
335
+ const response = {
336
+ error: 'Une erreur est survenue',
337
+ errorId,
338
+ timestamp: new Date().toISOString()
339
+ };
340
+
341
+ if (isDev) {
342
+ response.details = error.message;
343
+ response.stack = error.stack;
344
+ }
345
+
346
+ const statusCode = error.statusCode || error.status || 500;
347
+ res.status(statusCode).json(response);
348
+ }
349
+
350
+ // Validation améliorée des données de requête
351
+ validateRequestInput(req) {
352
+ const maxParamLength = this.securityConfig.maxParamLength;
353
+
354
+ // Validation avec limite de profondeur pour éviter les attaques par récursion
355
+ if (req.body) {
356
+ this.validateObject(req.body, 'body', maxParamLength, 0, 5);
357
+ }
358
+
359
+ if (req.query) {
360
+ this.validateObject(req.query, 'query', maxParamLength, 0, 3);
361
+ }
362
+
363
+ if (req.params) {
364
+ this.validateObject(req.params, 'params', maxParamLength, 0, 2);
365
+ }
366
+
367
+ // Validation des headers sensibles
368
+ this.validateHeaders(req);
369
+ }
370
+
371
+ // Validation récursive avec protection contre les attaques
372
+ validateObject(obj, type, maxLength, depth = 0, maxDepth = 5) {
373
+ if (depth > maxDepth) {
374
+ throw new Error(`Objet ${type} trop profond`);
375
+ }
376
+
377
+ const keys = Object.keys(obj);
378
+ if (keys.length > 100) {
379
+ throw new Error(`Trop de propriétés dans ${type}`);
380
+ }
381
+
382
+ for (const [key, value] of Object.entries(obj)) {
383
+ // Validation de la clé
384
+ if (key.length > 100) {
385
+ throw new Error(`Nom de propriété ${type}.${key} trop long`);
386
+ }
387
+
388
+ if (typeof value === 'string') {
389
+ if (value.length > maxLength) {
390
+ throw new Error(`Paramètre ${type}.${key} trop long`);
391
+ }
392
+
393
+ // Détection améliorée de tentatives d'injection
394
+ if (this.containsDangerousPatterns(value)) {
395
+ throw new Error(`Paramètre ${type}.${key} contient du contenu suspect`);
396
+ }
397
+ } else if (value && typeof value === 'object') {
398
+ this.validateObject(value, `${type}.${key}`, maxLength, depth + 1, maxDepth);
399
+ }
400
+ }
401
+ }
402
+
403
+ // Validation des headers
404
+ validateHeaders(req) {
405
+ const dangerousHeaders = ['x-forwarded-host', 'x-original-url', 'x-rewrite-url'];
406
+
407
+ dangerousHeaders.forEach(header => {
408
+ const value = req.get(header);
409
+ if (value && this.containsDangerousPatterns(value)) {
410
+ this.app.logger.log('security', 'Dangerous header detected', `${header}: ${value}`);
411
+ throw new Error('En-tête de requête suspect');
412
+ }
413
+ });
414
+ }
415
+
416
+ // Génération d'ID unique pour les routes
417
+ generateRouteId(method, path) {
418
+ return crypto.createHash('md5').update(`${method}:${path}:${Date.now()}`).digest('hex').substring(0, 8);
419
+ }
420
+
421
+ // Génération d'ID unique pour les erreurs
422
+ generateErrorId() {
423
+ return crypto.randomBytes(4).toString('hex').toUpperCase();
424
+ }
425
+
426
+ // Nettoyage des options amélioré
427
+ sanitizeOptions(options) {
428
+ const sanitized = {};
429
+ const allowedKeys = [
430
+ 'description', 'middleware', 'rateLimit', 'auth', 'createdBy',
431
+ 'timeout', 'clientId', 'security', 'cache'
432
+ ];
433
+
434
+ allowedKeys.forEach(key => {
435
+ if (options[key] !== undefined) {
436
+ // Validation spécifique par type d'option
437
+ switch (key) {
438
+ case 'timeout':
439
+ sanitized[key] = Math.min(Math.max(parseInt(options[key]) || 30000, 1000), 300000);
440
+ break;
441
+ case 'rateLimit':
442
+ if (typeof options[key] === 'object') {
443
+ sanitized[key] = {
444
+ window: Math.min(options[key].window || 60000, 3600000),
445
+ max: Math.min(options[key].max || 100, 10000)
446
+ };
447
+ }
448
+ break;
449
+ default:
450
+ sanitized[key] = options[key];
451
+ }
452
+ }
453
+ });
454
+
455
+ return sanitized;
456
+ }
457
+
458
+ // Nettoyage périodique optimisé
459
+ startCacheCleanup() {
460
+ // Nettoyage toutes les 5 minutes
461
+ setInterval(() => {
462
+ this.cleanupCache();
463
+ }, 300000);
464
+ }
465
+
466
+ cleanupCache() {
467
+ const now = Date.now();
468
+ let cleaned = 0;
469
+
470
+ // Nettoyage du cache rate limiting
471
+ if (this.rateLimitCache) {
472
+ for (const [key, value] of this.rateLimitCache.entries()) {
473
+ if (value.resetTime && now > value.resetTime) {
474
+ this.rateLimitCache.delete(key);
475
+ cleaned++;
476
+ }
477
+ }
478
+ }
479
+
480
+ // Nettoyage du cache de validation des chemins
481
+ if (this.pathValidationCache && this.pathValidationCache.size > 1000) {
482
+ this.pathValidationCache.clear();
483
+ cleaned += 1000;
484
+ }
485
+
486
+ // Nettoyage du cache des méthodes
487
+ if (this.methodsCache && this.methodsCache.size > 1000) {
488
+ this.methodsCache.clear();
489
+ cleaned += 1000;
490
+ }
491
+
492
+ if (cleaned > 0) {
493
+ this.app.logger.log('maintenance', 'Cache cleaned', `${cleaned} entries removed`);
494
+ }
495
+ }
496
+
497
+ async createRoute(method, path, handler, options = {}) {
498
+
499
+ try {
500
+ // Validation sécurisée des entrées
501
+ const validated = this.validateRouteInput(method, path, handler);
502
+ method = validated.method;
503
+ path = validated.path;
504
+ handler = validated.handler;
505
+
506
+ // Vérifie les limites de création de routes
507
+ if (this.dynamicRoutes.size >= this.maxDynamicRoutes) {
508
+ throw new Error('Limite de routes dynamiques atteinte');
509
+ }
510
+
511
+ // Rate limiting pour la création de routes
512
+ if (!this.checkRateLimit()) {
513
+ throw new Error('Trop de tentatives de création de routes');
514
+ }
515
+
516
+ // Hook de sécurité avant création
517
+ if (this.app.plugins) {
518
+ await this.app.plugins.executeHook('route:security-check', method, path, handler, options);
519
+ }
520
+
521
+ if (this.routeExists(method, path)) {
522
+ this.app.logger.log('warning', 'Route already exists', `${method.toUpperCase()} ${path}`);
523
+ return this.app;
524
+ }
525
+
526
+ // Wrapper sécurisé pour le handler
527
+ const secureHandler = this.createSecureHandler(handler, method, path);
528
+
529
+ if (Array.isArray(secureHandler)) {
530
+ this.app.app[method](path, ...secureHandler);
531
+ } else {
532
+ this.app.app[method](path, secureHandler);
533
+ }
534
+
535
+ const routeKey = `${method}:${path}`;
536
+ this.dynamicRoutes.set(routeKey, {
537
+ method,
538
+ path,
539
+ handler: secureHandler,
540
+ options: this.sanitizeOptions(options),
541
+ createdAt: new Date().toISOString(),
542
+ createdBy: options.createdBy || 'system'
543
+ });
544
+
545
+ this.app.logger.log('create', 'Route created dynamically', `${method.toUpperCase()} ${path}`);
546
+
547
+ if (this.app.plugins) {
548
+ await this.app.plugins.executeHook('route:created', method, path, secureHandler, options);
549
+ }
550
+
551
+ if (this.app.options.isDev && this.app.devServer) {
552
+ this.app.devServer.broadcast({
553
+ type: 'route-created',
554
+ method: method.toUpperCase(),
555
+ path,
556
+ timestamp: new Date().toISOString()
557
+ });
558
+ }
559
+
560
+ return this.app;
561
+ } catch (error) {
562
+ this.app.logger.log('error', 'Error creating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
563
+ throw error; // Re-throw pour une meilleure gestion d'erreur
564
+ }
565
+ }
566
+
567
+ // Crée un wrapper sécurisé pour les handlers
568
+ createSecureHandler(handler, method, path) {
569
+ const wrapHandler = (originalHandler) => {
570
+ return async (req, res, next) => {
571
+ try {
572
+ // Ajoute des headers de sécurité
573
+ res.setHeader('X-Content-Type-Options', 'nosniff');
574
+ res.setHeader('X-Frame-Options', 'DENY');
575
+
576
+ // Validation des paramètres d'entrée
577
+ this.validateRequestInput(req);
578
+
579
+ // Log de sécurité
580
+ this.app.logger.log('security', 'Route accessed', `${method.toUpperCase()} ${path} from ${req.ip}`);
581
+
582
+ // Exécute le handler original
583
+ await originalHandler(req, res, next);
584
+ } catch (error) {
585
+ this.app.logger.log('error', 'Handler error', `${method.toUpperCase()} ${path} → ${error.message}`);
586
+
587
+ // Ne pas exposer les détails d'erreur en production
588
+ if (process.env.NODE_ENV === 'production') {
589
+ res.status(500).json({ error: 'Erreur serveur interne' });
590
+ } else {
591
+ res.status(500).json({ error: error.message, stack: error.stack });
592
+ }
593
+ }
594
+ };
595
+ };
596
+
597
+ if (Array.isArray(handler)) {
598
+ return handler.map(h => wrapHandler(h));
599
+ } else {
600
+ return wrapHandler(handler);
601
+ }
602
+ }
603
+
604
+ // Validation des données de requête
605
+ validateRequestInput(req) {
606
+ // Limite la taille des paramètres
607
+ const maxParamLength = 1000;
608
+
609
+ if (req.body) {
610
+ this.validateObject(req.body, 'body', maxParamLength);
611
+ }
612
+
613
+ if (req.query) {
614
+ this.validateObject(req.query, 'query', maxParamLength);
615
+ }
616
+
617
+ if (req.params) {
618
+ this.validateObject(req.params, 'params', maxParamLength);
619
+ }
620
+ }
621
+
622
+ validateObject(obj, type, maxLength) {
623
+ for (const [key, value] of Object.entries(obj)) {
624
+ if (typeof value === 'string') {
625
+ if (value.length > maxLength) {
626
+ throw new Error(`Paramètre ${type}.${key} trop long`);
627
+ }
628
+
629
+ // Détection de tentatives d'injection
630
+ if (/[<>\"'`]|javascript:|data:|vbscript:|onload|onerror/i.test(value)) {
631
+ throw new Error(`Paramètre ${type}.${key} contient du contenu suspect`);
632
+ }
633
+ }
634
+ }
635
+ }
636
+
637
+ // Nettoyage des options
638
+ sanitizeOptions(options) {
639
+ const sanitized = {};
640
+
641
+ const allowedKeys = ['description', 'middleware', 'rateLimit', 'auth', 'createdBy'];
642
+
643
+ for (const key of allowedKeys) {
644
+ if (options[key] !== undefined) {
645
+ sanitized[key] = options[key];
646
+ }
647
+ }
648
+
649
+ return sanitized;
650
+ }
651
+
652
+ async deleteRoute(method, path) {
653
+ try {
654
+ method = method.toLowerCase();
655
+ const routeKey = `${method}:${path}`;
656
+
657
+ if (!this.dynamicRoutes.has(routeKey) && !this.routeExists(method, path)) {
658
+ this.app.logger.log('warning', 'Route not found', `${method.toUpperCase()} ${path}`);
659
+ return this.app;
660
+ }
661
+
662
+ this.removeRouteFromRouter(method, path);
663
+ this.dynamicRoutes.delete(routeKey);
664
+
665
+ this.app.logger.log('delete', 'Route deleted dynamically', `${method.toUpperCase()} ${path}`);
666
+
667
+ if (this.app.options.isDev && this.app.devServer) {
668
+ this.app.devServer.broadcast({
669
+ type: 'route-deleted',
670
+ method: method.toUpperCase(),
671
+ path,
672
+ timestamp: new Date().toISOString()
673
+ });
674
+ }
675
+
676
+ return this.app;
677
+ } catch (error) {
678
+ this.app.logger.log('error', 'Error deleting route', `${method?.toUpperCase()} ${path} → ${error.message}`);
679
+ return this.app;
680
+ }
681
+ }
682
+
683
+ async updateRoute(method, path, newHandler) {
684
+ try {
685
+ await this.deleteRoute(method, path);
686
+ await this.createRoute(method, path, newHandler);
687
+
688
+ this.app.logger.log('reload', 'Route updated', `${method.toUpperCase()} ${path}`);
689
+ return this.app;
690
+ } catch (error) {
691
+ this.app.logger.log('error', 'Error updating route', `${method?.toUpperCase()} ${path} → ${error.message}`);
692
+ return this.app;
693
+ }
694
+ }
695
+
696
+ routeExists(method, path) {
697
+ if (!this.app.app._router) return false;
698
+
699
+ return this.app.app._router.stack.some(layer => {
700
+ if (layer.route) {
701
+ const routeMethods = Object.keys(layer.route.methods);
702
+ return layer.route.path === path && routeMethods.includes(method.toLowerCase());
703
+ }
704
+ return false;
705
+ });
706
+ }
707
+
708
+ removeRouteFromRouter(method, path) {
709
+ if (!this.app.app._router) return;
710
+
711
+ this.app.app._router.stack = this.app.app._router.stack.filter(layer => {
712
+ if (layer.route) {
713
+ const routeMethods = Object.keys(layer.route.methods);
714
+ const shouldRemove = layer.route.path === path && routeMethods.includes(method.toLowerCase());
715
+
716
+ if (shouldRemove) {
717
+ this.app.logger.log('dev', 'Route removed from Express router', `🗑️ ${method.toUpperCase()} ${path}`);
718
+ }
719
+
720
+ return !shouldRemove;
721
+ }
722
+ return true;
723
+ });
724
+ }
725
+
726
+ loadRoutes(routesDir = this.options.routesDir) {
727
+ const routesPath = path.join(process.cwd(), routesDir);
728
+
729
+ if (!fs.existsSync(routesPath)) {
730
+ this.app.logger.log('warning', 'Routes directory not found', `📁 ${routesDir}`);
731
+ this.createRoutesDirectory(routesPath);
732
+ return this.app;
733
+ }
734
+
735
+ this.app.logger.log('info', 'Scanning routes...', `📂 ${routesDir}`);
736
+ this.scanDirectory(routesPath, routesPath);
737
+
738
+ // Vérifier si la route / existe
739
+ if (!this.routeExists('get', '/')) {
740
+ this.app.logger.log('warning', 'No root route found', 'Create routes/index.js to define the home page');
741
+ }
742
+
743
+ return this.app;
744
+ }
745
+
746
+ scanDirectory(dirPath, basePath) {
747
+ const files = fs.readdirSync(dirPath);
748
+
749
+ files.forEach(file => {
750
+ const filePath = path.join(dirPath, file);
751
+ const stat = fs.statSync(filePath);
752
+
753
+ if (stat.isDirectory()) {
754
+ this.scanDirectory(filePath, basePath);
755
+ } else if (file.endsWith('.js')) {
756
+ this.loadRouteFile(filePath, basePath);
757
+ }
758
+ });
759
+ }
760
+
761
+ loadRouteFile(filePath, basePath) {
762
+ try {
763
+ // Validation du chemin de fichier pour éviter les attaques par traversée
764
+ const resolvedPath = path.resolve(filePath);
765
+ const resolvedBase = path.resolve(basePath);
766
+
767
+ if (!resolvedPath.startsWith(resolvedBase)) {
768
+ throw new Error('Tentative d\'accès à un fichier en dehors du répertoire autorisé');
769
+ }
770
+
771
+ // Vérifie l'extension du fichier
772
+ if (!filePath.endsWith('.js')) {
773
+ throw new Error('Seuls les fichiers .js sont autorisés');
774
+ }
775
+
776
+ delete require.cache[require.resolve(filePath)];
777
+ const routeModule = require(filePath);
778
+
779
+ const relativePath = path.relative(basePath, filePath);
780
+ const routePath = this.filePathToRoute(relativePath);
781
+
782
+ this.routeMap.set(filePath, routePath);
783
+
784
+ if (typeof routeModule === 'function') {
785
+ // Wrapper sécurisé pour les fonctions de route
786
+ const secureModule = (app) => {
787
+ try {
788
+ routeModule(app);
789
+ } catch (error) {
790
+ this.app.logger.log('error', 'Route module error', error.message);
791
+ }
792
+ };
793
+ secureModule(this.app.app);
794
+ } else if (routeModule.router) {
795
+ this.app.app.use(routePath, routeModule.router);
796
+ } else if (routeModule.get || routeModule.post || routeModule.put || routeModule.delete || routeModule.patch) {
797
+ this.setupRouteHandlers(routePath, routeModule);
798
+ } else {
799
+ this.app.logger.log('warning', 'Invalid route module', `${path.basename(filePath)} - No valid exports found`);
800
+ return;
801
+ }
802
+
803
+ const fileName = path.basename(filePath);
804
+ this.app.logger.log('route', 'Route loaded', `${fileName} → ${routePath}`);
805
+ } catch (error) {
806
+ const fileName = path.basename(filePath);
807
+ this.app.logger.log('error', 'Failed to load', `${fileName} → ${error.message}`);
808
+ // En production, ne pas arrêter l'application pour un fichier de route défaillant
809
+ if (process.env.NODE_ENV !== 'production') {
810
+ throw error;
811
+ }
812
+ }
813
+ }
814
+
815
+ createRoutesDirectory(routesPath) {
816
+ try {
817
+ // Créer le dossier routes
818
+ fs.mkdirSync(routesPath, { recursive: true });
819
+ this.app.logger.log('create', 'Routes directory created', `📁 ${path.relative(process.cwd(), routesPath)}`);
820
+
821
+ // Créer le fichier index.js avec une route par défaut
822
+ const indexPath = path.join(routesPath, 'index.js');
823
+ const defaultIndexContent = `// Route principale de l'application
824
+ module.exports = {
825
+ get: (req, res) => {
826
+ res.render('index', {
827
+ title: 'Veko.js - Ultra modern framework',
828
+ message: 'Welcome to Veko.js! 🚀',
829
+ description: 'Your application is running successfully.'
830
+ });
831
+ }
832
+ };`;
833
+
834
+ fs.writeFileSync(indexPath, defaultIndexContent, 'utf8');
835
+ this.app.logger.log('create', 'Default index route created', `📄 ${path.relative(process.cwd(), indexPath)}`);
836
+
837
+ // Créer également une vue index.ejs par défaut si elle n'existe pas
838
+ this.createDefaultIndexView();
839
+
840
+ } catch (error) {
841
+ this.app.logger.log('error', 'Error creating routes directory', error.message);
842
+ }
843
+ }
844
+
845
+ createDefaultIndexView() {
846
+ const viewsPath = path.join(process.cwd(), this.app.options.viewsDir);
847
+ const indexViewPath = path.join(viewsPath, 'index.ejs');
848
+
849
+ if (!fs.existsSync(indexViewPath)) {
850
+ // Créer le dossier views s'il n'existe pas
851
+ if (!fs.existsSync(viewsPath)) {
852
+ fs.mkdirSync(viewsPath, { recursive: true });
853
+ this.app.logger.log('create', 'Views directory created', `📁 ${path.relative(process.cwd(), viewsPath)}`);
854
+ }
855
+
856
+ const defaultViewContent = `<% layout.css = ['/css/home.css'] %>
857
+ <% layout.js = ['/js/home.js'] %>
858
+
859
+ <div class="hero">
860
+ <div class="hero-content">
861
+ <h1><%= title %></h1>
862
+ <p class="lead"><%= message %></p>
863
+ <p><%= description %></p>
864
+
865
+ <div class="features">
866
+ <div class="feature">
867
+ <h3>🚀 Ultra Rapide</h3>
868
+ <p>Framework optimisé pour les performances</p>
869
+ </div>
870
+ <div class="feature">
871
+ <h3>🔥 Hot Reload</h3>
872
+ <p>Rechargement automatique en développement</p>
873
+ </div>
874
+ <div class="feature">
875
+ <h3>🎨 Layouts</h3>
876
+ <p>Système de mise en page intégré</p>
877
+ </div>
878
+ <div class="feature">
879
+ <h3>🔌 Plugins</h3>
880
+ <p>Architecture extensible avec plugins</p>
881
+ </div>
882
+ </div>
883
+
884
+ <div class="actions">
885
+ <a href="/docs" class="btn btn-primary">Documentation</a>
886
+ <a href="/examples" class="btn btn-secondary">Exemples</a>
887
+ </div>
888
+ </div>
889
+ </div>
890
+
891
+ <% layout.section('scripts', \`
892
+ <script>
893
+ console.log('🎉 Veko.js app loaded successfully!');
894
+ </script>
895
+ \`) %>`;
896
+
897
+ fs.writeFileSync(indexViewPath, defaultViewContent, 'utf8');
898
+ this.app.logger.log('create', 'Default index view created', `📄 ${path.relative(process.cwd(), indexViewPath)}`);
899
+ }
900
+ }
901
+
902
+ filePathToRoute(filePath) {
903
+ let route = filePath
904
+ .replace(/\\/g, '/')
905
+ .replace(/\.js$/, '')
906
+ .replace(/\/index$/, '')
907
+ .replace(/\[([^\]]+)\]/g, ':$1');
908
+
909
+ if (!route.startsWith('/')) {
910
+ route = '/' + route;
911
+ }
912
+
913
+ // Si le fichier est index.js à la racine, la route devient '/'
914
+ if (route === '' || route === '/') {
915
+ route = '/';
916
+ }
917
+
918
+ return route;
919
+ }
920
+
921
+ setupRouteHandlers(routePath, handlers) {
922
+ if (handlers.get) this.app.app.get(routePath, handlers.get);
923
+ if (handlers.post) this.app.app.post(routePath, handlers.post);
924
+ if (handlers.put) this.app.app.put(routePath, handlers.put);
925
+ if (handlers.delete) this.app.app.delete(routePath, handlers.delete);
926
+ if (handlers.patch) this.app.app.patch(routePath, handlers.patch);
927
+ }
928
+
929
+ listRoutes() {
930
+ const routes = [];
931
+
932
+ this.routeMap.forEach((routePath, filePath) => {
933
+ routes.push({
934
+ type: 'file',
935
+ path: routePath,
936
+ source: path.relative(process.cwd(), filePath),
937
+ methods: this.getRouteMethods(routePath)
938
+ });
939
+ });
940
+
941
+ this.dynamicRoutes.forEach((routeInfo, routeKey) => {
942
+ routes.push({
943
+ type: 'dynamic',
944
+ path: routeInfo.path,
945
+ method: routeInfo.method.toUpperCase(),
946
+ createdAt: routeInfo.createdAt
947
+ });
948
+ });
949
+
950
+ return routes;
951
+ }
952
+
953
+ // Optimisation: cache des méthodes de route
954
+ getRouteMethods(routePath) {
955
+ const cacheKey = `methods:${routePath}`;
956
+
957
+ if (this.methodsCache && this.methodsCache.has(cacheKey)) {
958
+ return this.methodsCache.get(cacheKey);
959
+ }
960
+
961
+ if (!this.app.app._router) return [];
962
+
963
+ const methods = new Set();
964
+ this.app.app._router.stack.forEach(layer => {
965
+ if (layer.route && layer.route.path === routePath) {
966
+ Object.keys(layer.route.methods).forEach(method => {
967
+ methods.add(method.toUpperCase());
968
+ });
969
+ }
970
+ });
971
+
972
+ const result = Array.from(methods);
973
+
974
+ // Cache le résultat
975
+ if (!this.methodsCache) {
976
+ this.methodsCache = new Map();
977
+ }
978
+ this.methodsCache.set(cacheKey, result);
979
+
980
+ return result;
981
+ }
982
+
983
+ // Nettoyage périodique du cache
984
+ cleanupCache() {
985
+ if (this.rateLimitCache) {
986
+ const now = Date.now();
987
+ for (const [key, value] of this.rateLimitCache.entries()) {
988
+ if (now > value.resetTime) {
989
+ this.rateLimitCache.delete(key);
990
+ }
991
+ }
992
+ }
993
+
994
+ if (this.methodsCache && this.methodsCache.size > 1000) {
995
+ this.methodsCache.clear();
996
+ }
997
+ }
998
+ }
999
+
1000
+ module.exports = RouteManager;