wu-framework 1.0.5 → 1.0.6
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/package.json +1 -1
- package/src/core/wu-cache.js +87 -15
- package/src/core/wu-event-bus.js +147 -58
- package/src/core/wu-manifest.js +139 -8
- package/src/core/wu-plugin.js +201 -71
- package/src/index.js +1 -1
package/package.json
CHANGED
package/src/core/wu-cache.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 💾 WU-CACHE: INTERNAL
|
|
2
|
+
* 💾 WU-CACHE: SECURE INTERNAL CACHING
|
|
3
3
|
*
|
|
4
|
-
* Sistema de caché INTERNO
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
* Features:
|
|
10
|
-
* - Cache persistente (localStorage/sessionStorage)
|
|
11
|
-
* - Cache en memoria (Map)
|
|
12
|
-
* - TTL (Time To Live) configurable
|
|
13
|
-
* - LRU (Least Recently Used) eviction
|
|
4
|
+
* Sistema de caché INTERNO con rate limiting
|
|
5
|
+
* - Rate limiting para prevenir abuso
|
|
6
|
+
* - Cache persistente y en memoria
|
|
7
|
+
* - TTL y LRU eviction
|
|
14
8
|
*
|
|
15
9
|
* ⚠️ USO INTERNO: No exponer en API pública
|
|
16
10
|
*/
|
|
@@ -22,15 +16,26 @@ export class WuCache {
|
|
|
22
16
|
maxItems: options.maxItems || 100,
|
|
23
17
|
defaultTTL: options.defaultTTL || 3600000, // 1 hour
|
|
24
18
|
persistent: options.persistent !== false,
|
|
25
|
-
storage: options.storage || 'memory',
|
|
19
|
+
storage: options.storage || 'memory',
|
|
26
20
|
compression: options.compression || false
|
|
27
21
|
};
|
|
28
22
|
|
|
23
|
+
// 🔐 Rate limiting configuration
|
|
24
|
+
this.rateLimiting = {
|
|
25
|
+
enabled: options.rateLimiting !== false,
|
|
26
|
+
maxOpsPerSecond: options.maxOpsPerSecond || 100,
|
|
27
|
+
windowMs: 1000, // 1 second window
|
|
28
|
+
cooldownMs: options.cooldownMs || 5000, // 5 second cooldown after limit
|
|
29
|
+
operations: [],
|
|
30
|
+
inCooldown: false,
|
|
31
|
+
cooldownUntil: 0
|
|
32
|
+
};
|
|
33
|
+
|
|
29
34
|
// Memory cache
|
|
30
35
|
this.memoryCache = new Map();
|
|
31
36
|
|
|
32
37
|
// LRU tracking
|
|
33
|
-
this.accessOrder = new Map();
|
|
38
|
+
this.accessOrder = new Map();
|
|
34
39
|
|
|
35
40
|
// Statistics
|
|
36
41
|
this.stats = {
|
|
@@ -38,10 +43,67 @@ export class WuCache {
|
|
|
38
43
|
misses: 0,
|
|
39
44
|
sets: 0,
|
|
40
45
|
evictions: 0,
|
|
41
|
-
size: 0
|
|
46
|
+
size: 0,
|
|
47
|
+
rateLimited: 0 // 🔐 Contador de operaciones rechazadas
|
|
42
48
|
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 🔐 CHECK RATE LIMIT: Verificar si la operación está permitida
|
|
53
|
+
* @returns {boolean} true si la operación está permitida
|
|
54
|
+
*/
|
|
55
|
+
_checkRateLimit() {
|
|
56
|
+
if (!this.rateLimiting.enabled) return true;
|
|
57
|
+
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
|
|
60
|
+
// Verificar si estamos en cooldown
|
|
61
|
+
if (this.rateLimiting.inCooldown) {
|
|
62
|
+
if (now < this.rateLimiting.cooldownUntil) {
|
|
63
|
+
this.stats.rateLimited++;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Cooldown terminado
|
|
67
|
+
this.rateLimiting.inCooldown = false;
|
|
68
|
+
this.rateLimiting.operations = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Limpiar operaciones antiguas (fuera de la ventana)
|
|
72
|
+
const windowStart = now - this.rateLimiting.windowMs;
|
|
73
|
+
this.rateLimiting.operations = this.rateLimiting.operations.filter(
|
|
74
|
+
ts => ts > windowStart
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Verificar límite
|
|
78
|
+
if (this.rateLimiting.operations.length >= this.rateLimiting.maxOpsPerSecond) {
|
|
79
|
+
// Activar cooldown
|
|
80
|
+
this.rateLimiting.inCooldown = true;
|
|
81
|
+
this.rateLimiting.cooldownUntil = now + this.rateLimiting.cooldownMs;
|
|
82
|
+
this.stats.rateLimited++;
|
|
83
|
+
console.warn(`[WuCache] 🚫 Rate limit exceeded. Cooldown for ${this.rateLimiting.cooldownMs}ms`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
43
86
|
|
|
44
|
-
|
|
87
|
+
// Registrar operación
|
|
88
|
+
this.rateLimiting.operations.push(now);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 🔐 GET RATE LIMIT STATUS
|
|
94
|
+
*/
|
|
95
|
+
getRateLimitStatus() {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
return {
|
|
98
|
+
enabled: this.rateLimiting.enabled,
|
|
99
|
+
inCooldown: this.rateLimiting.inCooldown,
|
|
100
|
+
cooldownRemaining: this.rateLimiting.inCooldown
|
|
101
|
+
? Math.max(0, this.rateLimiting.cooldownUntil - now)
|
|
102
|
+
: 0,
|
|
103
|
+
currentOps: this.rateLimiting.operations.length,
|
|
104
|
+
maxOps: this.rateLimiting.maxOpsPerSecond,
|
|
105
|
+
rateLimited: this.stats.rateLimited
|
|
106
|
+
};
|
|
45
107
|
}
|
|
46
108
|
|
|
47
109
|
/**
|
|
@@ -50,6 +112,11 @@ export class WuCache {
|
|
|
50
112
|
* @returns {*} Valor cacheado o null
|
|
51
113
|
*/
|
|
52
114
|
get(key) {
|
|
115
|
+
// 🔐 Check rate limit
|
|
116
|
+
if (!this._checkRateLimit()) {
|
|
117
|
+
return null; // Silently fail on rate limit
|
|
118
|
+
}
|
|
119
|
+
|
|
53
120
|
// 1. Buscar en memoria
|
|
54
121
|
if (this.memoryCache.has(key)) {
|
|
55
122
|
const entry = this.memoryCache.get(key);
|
|
@@ -92,6 +159,11 @@ export class WuCache {
|
|
|
92
159
|
* @returns {boolean}
|
|
93
160
|
*/
|
|
94
161
|
set(key, value, ttl) {
|
|
162
|
+
// 🔐 Check rate limit
|
|
163
|
+
if (!this._checkRateLimit()) {
|
|
164
|
+
return false; // Reject on rate limit
|
|
165
|
+
}
|
|
166
|
+
|
|
95
167
|
try {
|
|
96
168
|
const entry = {
|
|
97
169
|
key,
|
package/src/core/wu-event-bus.js
CHANGED
|
@@ -1,46 +1,156 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 📡 WU-EVENT-BUS:
|
|
2
|
+
* 📡 WU-EVENT-BUS: SECURE PUB/SUB SYSTEM
|
|
3
3
|
*
|
|
4
4
|
* Sistema de eventos para comunicación entre microfrontends
|
|
5
|
-
* - Pub/Sub pattern
|
|
5
|
+
* - Pub/Sub pattern con validación de origen
|
|
6
6
|
* - Event namespaces
|
|
7
7
|
* - Wildcards
|
|
8
8
|
* - Event replay
|
|
9
|
-
* -
|
|
9
|
+
* - Verificación de apps autorizadas
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export class WuEventBus {
|
|
13
13
|
constructor() {
|
|
14
|
-
this.listeners = new Map();
|
|
15
|
-
this.history = [];
|
|
14
|
+
this.listeners = new Map();
|
|
15
|
+
this.history = [];
|
|
16
|
+
|
|
17
|
+
// 🔐 SEGURIDAD: Registro de apps autorizadas con tokens
|
|
18
|
+
this.authorizedApps = new Map(); // appName -> { token, permissions }
|
|
19
|
+
this.trustedEvents = new Set(['wu:*', 'system:*']); // Eventos del sistema
|
|
20
|
+
|
|
16
21
|
this.config = {
|
|
17
22
|
maxHistory: 100,
|
|
18
23
|
enableReplay: true,
|
|
19
24
|
enableWildcards: true,
|
|
20
|
-
logEvents: false
|
|
25
|
+
logEvents: false,
|
|
26
|
+
// 🔐 Opciones de seguridad
|
|
27
|
+
strictMode: false, // Si true, rechaza eventos de apps no autorizadas
|
|
28
|
+
validateOrigin: true // Valida que appName sea una app registrada
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
this.stats = {
|
|
24
32
|
emitted: 0,
|
|
25
|
-
subscriptions: 0
|
|
33
|
+
subscriptions: 0,
|
|
34
|
+
rejected: 0 // Eventos rechazados por seguridad
|
|
26
35
|
};
|
|
36
|
+
}
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
/**
|
|
39
|
+
* 🔐 REGISTER APP: Registrar app autorizada para emitir eventos
|
|
40
|
+
* @param {string} appName - Nombre de la app
|
|
41
|
+
* @param {Object} options - { permissions: ['event:*'], token }
|
|
42
|
+
* @returns {string} Token de autorización
|
|
43
|
+
*/
|
|
44
|
+
registerApp(appName, options = {}) {
|
|
45
|
+
const token = options.token || this._generateToken();
|
|
46
|
+
|
|
47
|
+
this.authorizedApps.set(appName, {
|
|
48
|
+
token,
|
|
49
|
+
permissions: options.permissions || ['*'], // Por defecto puede emitir todo
|
|
50
|
+
registeredAt: Date.now()
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return token;
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
/**
|
|
32
|
-
*
|
|
57
|
+
* 🔓 UNREGISTER APP: Desregistrar app
|
|
58
|
+
* @param {string} appName
|
|
59
|
+
*/
|
|
60
|
+
unregisterApp(appName) {
|
|
61
|
+
this.authorizedApps.delete(appName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 🔐 VALIDATE ORIGIN: Verificar que el emisor está autorizado
|
|
66
|
+
* @param {string} eventName
|
|
67
|
+
* @param {string} appName
|
|
68
|
+
* @param {string} token
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
_validateOrigin(eventName, appName, token) {
|
|
72
|
+
// Eventos del sistema siempre permitidos
|
|
73
|
+
if (this._isSystemEvent(eventName)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Si no está en modo estricto, permitir todo
|
|
78
|
+
if (!this.config.strictMode) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Verificar que la app esté registrada
|
|
83
|
+
const appInfo = this.authorizedApps.get(appName);
|
|
84
|
+
if (!appInfo) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Verificar token si se proporciona
|
|
89
|
+
if (token && appInfo.token !== token) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Verificar permisos
|
|
94
|
+
return this._hasPermission(appInfo.permissions, eventName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 🔐 HAS PERMISSION: Verificar si la app tiene permiso para el evento
|
|
99
|
+
*/
|
|
100
|
+
_hasPermission(permissions, eventName) {
|
|
101
|
+
if (permissions.includes('*')) return true;
|
|
102
|
+
|
|
103
|
+
return permissions.some(pattern => {
|
|
104
|
+
if (pattern === eventName) return true;
|
|
105
|
+
if (pattern.includes('*')) {
|
|
106
|
+
return this.matchesWildcard(eventName, pattern);
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 🔐 IS SYSTEM EVENT: Verificar si es un evento del sistema
|
|
114
|
+
*/
|
|
115
|
+
_isSystemEvent(eventName) {
|
|
116
|
+
return eventName.startsWith('wu:') ||
|
|
117
|
+
eventName.startsWith('system:') ||
|
|
118
|
+
eventName.startsWith('app:');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 🔐 GENERATE TOKEN: Generar token único
|
|
123
|
+
*/
|
|
124
|
+
_generateToken() {
|
|
125
|
+
return `wu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 📢 EMIT: Emitir evento con validación de origen
|
|
33
130
|
* @param {string} eventName - Nombre del evento
|
|
34
131
|
* @param {*} data - Datos del evento
|
|
35
|
-
* @param {Object} options -
|
|
132
|
+
* @param {Object} options - { appName, timestamp, meta, token }
|
|
36
133
|
*/
|
|
37
134
|
emit(eventName, data, options = {}) {
|
|
135
|
+
const appName = options.appName || 'unknown';
|
|
136
|
+
|
|
137
|
+
// 🔐 Validar origen si está habilitado
|
|
138
|
+
if (this.config.validateOrigin && this.config.strictMode) {
|
|
139
|
+
if (!this._validateOrigin(eventName, appName, options.token)) {
|
|
140
|
+
this.stats.rejected++;
|
|
141
|
+
console.warn(`[WuEventBus] 🚫 Event rejected: ${eventName} from ${appName} (unauthorized)`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
38
146
|
const event = {
|
|
39
147
|
name: eventName,
|
|
40
148
|
data,
|
|
41
149
|
timestamp: options.timestamp || Date.now(),
|
|
42
|
-
appName
|
|
43
|
-
meta: options.meta || {}
|
|
150
|
+
appName,
|
|
151
|
+
meta: options.meta || {},
|
|
152
|
+
// 🔐 Marcar si el origen fue verificado
|
|
153
|
+
verified: this.authorizedApps.has(appName)
|
|
44
154
|
};
|
|
45
155
|
|
|
46
156
|
// Agregar a historial
|
|
@@ -71,13 +181,11 @@ export class WuEventBus {
|
|
|
71
181
|
}
|
|
72
182
|
|
|
73
183
|
this.stats.emitted++;
|
|
184
|
+
return true;
|
|
74
185
|
}
|
|
75
186
|
|
|
76
187
|
/**
|
|
77
188
|
* 👂 ON: Suscribirse a evento
|
|
78
|
-
* @param {string} eventName - Nombre del evento (puede usar wildcards: 'app.*', '*.update')
|
|
79
|
-
* @param {Function} callback - Callback a ejecutar
|
|
80
|
-
* @returns {Function} Función para desuscribirse
|
|
81
189
|
*/
|
|
82
190
|
on(eventName, callback) {
|
|
83
191
|
if (!this.listeners.has(eventName)) {
|
|
@@ -87,48 +195,36 @@ export class WuEventBus {
|
|
|
87
195
|
this.listeners.get(eventName).add(callback);
|
|
88
196
|
this.stats.subscriptions++;
|
|
89
197
|
|
|
90
|
-
// Retornar función de desuscripción
|
|
91
198
|
return () => this.off(eventName, callback);
|
|
92
199
|
}
|
|
93
200
|
|
|
94
201
|
/**
|
|
95
202
|
* 🔇 OFF: Desuscribirse de evento
|
|
96
|
-
* @param {string} eventName - Nombre del evento
|
|
97
|
-
* @param {Function} callback - Callback a remover
|
|
98
203
|
*/
|
|
99
204
|
off(eventName, callback) {
|
|
100
205
|
const listeners = this.listeners.get(eventName);
|
|
101
206
|
if (listeners) {
|
|
102
207
|
listeners.delete(callback);
|
|
103
|
-
|
|
104
|
-
// Limpiar si no quedan listeners
|
|
105
208
|
if (listeners.size === 0) {
|
|
106
209
|
this.listeners.delete(eventName);
|
|
107
210
|
}
|
|
108
|
-
|
|
109
211
|
this.stats.subscriptions--;
|
|
110
212
|
}
|
|
111
213
|
}
|
|
112
214
|
|
|
113
215
|
/**
|
|
114
216
|
* 🎯 ONCE: Suscribirse una sola vez
|
|
115
|
-
* @param {string} eventName - Nombre del evento
|
|
116
|
-
* @param {Function} callback - Callback a ejecutar
|
|
117
|
-
* @returns {Function} Función para desuscribirse
|
|
118
217
|
*/
|
|
119
218
|
once(eventName, callback) {
|
|
120
219
|
const wrappedCallback = (event) => {
|
|
121
220
|
callback(event);
|
|
122
221
|
this.off(eventName, wrappedCallback);
|
|
123
222
|
};
|
|
124
|
-
|
|
125
223
|
return this.on(eventName, wrappedCallback);
|
|
126
224
|
}
|
|
127
225
|
|
|
128
226
|
/**
|
|
129
|
-
* 🌟 WILDCARD LISTENERS
|
|
130
|
-
* @param {string} eventName - Nombre del evento emitido
|
|
131
|
-
* @param {Object} event - Objeto del evento
|
|
227
|
+
* 🌟 WILDCARD LISTENERS
|
|
132
228
|
*/
|
|
133
229
|
notifyWildcardListeners(eventName, event) {
|
|
134
230
|
for (const [pattern, listeners] of this.listeners) {
|
|
@@ -145,41 +241,29 @@ export class WuEventBus {
|
|
|
145
241
|
}
|
|
146
242
|
|
|
147
243
|
/**
|
|
148
|
-
* 🎯 MATCHES WILDCARD
|
|
149
|
-
* @param {string} eventName - Nombre del evento
|
|
150
|
-
* @param {string} pattern - Patrón con wildcards
|
|
151
|
-
* @returns {boolean}
|
|
244
|
+
* 🎯 MATCHES WILDCARD
|
|
152
245
|
*/
|
|
153
246
|
matchesWildcard(eventName, pattern) {
|
|
154
|
-
// Si no hay wildcard, ya se procesó en listeners exactos
|
|
155
247
|
if (!pattern.includes('*')) return false;
|
|
156
|
-
|
|
157
|
-
// Convertir pattern a regex
|
|
158
248
|
const regexPattern = pattern
|
|
159
249
|
.replace(/\./g, '\\.')
|
|
160
250
|
.replace(/\*/g, '.*');
|
|
161
|
-
|
|
162
251
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
163
252
|
return regex.test(eventName);
|
|
164
253
|
}
|
|
165
254
|
|
|
166
255
|
/**
|
|
167
|
-
* 📝 ADD TO HISTORY
|
|
168
|
-
* @param {Object} event - Evento
|
|
256
|
+
* 📝 ADD TO HISTORY
|
|
169
257
|
*/
|
|
170
258
|
addToHistory(event) {
|
|
171
259
|
this.history.push(event);
|
|
172
|
-
|
|
173
|
-
// Mantener tamaño máximo
|
|
174
260
|
if (this.history.length > this.config.maxHistory) {
|
|
175
261
|
this.history.shift();
|
|
176
262
|
}
|
|
177
263
|
}
|
|
178
264
|
|
|
179
265
|
/**
|
|
180
|
-
* 🔄 REPLAY
|
|
181
|
-
* @param {string} eventNameOrPattern - Nombre o patrón de eventos a reproducir
|
|
182
|
-
* @param {Function} callback - Callback para cada evento
|
|
266
|
+
* 🔄 REPLAY
|
|
183
267
|
*/
|
|
184
268
|
replay(eventNameOrPattern, callback) {
|
|
185
269
|
const events = this.history.filter(event => {
|
|
@@ -189,8 +273,6 @@ export class WuEventBus {
|
|
|
189
273
|
return event.name === eventNameOrPattern;
|
|
190
274
|
});
|
|
191
275
|
|
|
192
|
-
console.log(`[WuEventBus] 🔄 Replaying ${events.length} events for ${eventNameOrPattern}`);
|
|
193
|
-
|
|
194
276
|
events.forEach(event => {
|
|
195
277
|
try {
|
|
196
278
|
callback(event);
|
|
@@ -201,13 +283,11 @@ export class WuEventBus {
|
|
|
201
283
|
}
|
|
202
284
|
|
|
203
285
|
/**
|
|
204
|
-
* 🧹 CLEAR HISTORY
|
|
205
|
-
* @param {string} eventNameOrPattern - Patrón de eventos a limpiar (opcional)
|
|
286
|
+
* 🧹 CLEAR HISTORY
|
|
206
287
|
*/
|
|
207
288
|
clearHistory(eventNameOrPattern) {
|
|
208
289
|
if (!eventNameOrPattern) {
|
|
209
290
|
this.history = [];
|
|
210
|
-
console.log('[WuEventBus] 🧹 Event history cleared');
|
|
211
291
|
return;
|
|
212
292
|
}
|
|
213
293
|
|
|
@@ -220,14 +300,14 @@ export class WuEventBus {
|
|
|
220
300
|
}
|
|
221
301
|
|
|
222
302
|
/**
|
|
223
|
-
* 📊 GET STATS
|
|
224
|
-
* @returns {Object}
|
|
303
|
+
* 📊 GET STATS
|
|
225
304
|
*/
|
|
226
305
|
getStats() {
|
|
227
306
|
return {
|
|
228
307
|
...this.stats,
|
|
229
308
|
activeListeners: this.listeners.size,
|
|
230
309
|
historySize: this.history.length,
|
|
310
|
+
authorizedApps: this.authorizedApps.size,
|
|
231
311
|
listenersByEvent: Array.from(this.listeners.entries()).map(([event, listeners]) => ({
|
|
232
312
|
event,
|
|
233
313
|
listeners: listeners.size
|
|
@@ -236,22 +316,31 @@ export class WuEventBus {
|
|
|
236
316
|
}
|
|
237
317
|
|
|
238
318
|
/**
|
|
239
|
-
* ⚙️ CONFIGURE
|
|
240
|
-
* @param {Object} config - Nueva configuración
|
|
319
|
+
* ⚙️ CONFIGURE
|
|
241
320
|
*/
|
|
242
321
|
configure(config) {
|
|
243
|
-
this.config = {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
322
|
+
this.config = { ...this.config, ...config };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 🔐 ENABLE STRICT MODE: Activar modo estricto de seguridad
|
|
327
|
+
*/
|
|
328
|
+
enableStrictMode() {
|
|
329
|
+
this.config.strictMode = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 🔓 DISABLE STRICT MODE
|
|
334
|
+
*/
|
|
335
|
+
disableStrictMode() {
|
|
336
|
+
this.config.strictMode = false;
|
|
247
337
|
}
|
|
248
338
|
|
|
249
339
|
/**
|
|
250
|
-
* 🗑️ REMOVE ALL
|
|
340
|
+
* 🗑️ REMOVE ALL
|
|
251
341
|
*/
|
|
252
342
|
removeAll() {
|
|
253
343
|
this.listeners.clear();
|
|
254
344
|
this.stats.subscriptions = 0;
|
|
255
|
-
console.log('[WuEventBus] 🗑️ All listeners removed');
|
|
256
345
|
}
|
|
257
346
|
}
|
package/src/core/wu-manifest.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 📋 WU-MANIFEST:
|
|
3
|
-
*
|
|
2
|
+
* 📋 WU-MANIFEST: SECURE MANIFEST SYSTEM
|
|
3
|
+
* Validación estricta de wu.json para seguridad
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class WuManifest {
|
|
@@ -8,10 +8,33 @@ export class WuManifest {
|
|
|
8
8
|
this.cache = new Map();
|
|
9
9
|
this.schemas = new Map();
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
this.
|
|
11
|
+
// 🔐 Configuración de seguridad
|
|
12
|
+
this.security = {
|
|
13
|
+
maxManifestSize: 100 * 1024, // 100KB máximo
|
|
14
|
+
maxNameLength: 50,
|
|
15
|
+
maxEntryLength: 200,
|
|
16
|
+
maxExports: 100,
|
|
17
|
+
maxImports: 50,
|
|
18
|
+
maxRoutes: 100,
|
|
19
|
+
// Patrones peligrosos en paths
|
|
20
|
+
dangerousPatterns: [
|
|
21
|
+
/\.\./, // Path traversal
|
|
22
|
+
/^\/etc\//, // System paths
|
|
23
|
+
/^\/proc\//,
|
|
24
|
+
/^file:\/\//, // File protocol
|
|
25
|
+
/javascript:/i, // JS injection
|
|
26
|
+
/data:/i, // Data URLs
|
|
27
|
+
/<script/i, // Script tags
|
|
28
|
+
/on\w+\s*=/i // Event handlers
|
|
29
|
+
],
|
|
30
|
+
// Dominios bloqueados
|
|
31
|
+
blockedDomains: [
|
|
32
|
+
'evil.com',
|
|
33
|
+
'malware.com'
|
|
34
|
+
]
|
|
35
|
+
};
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
this.defineSchema();
|
|
15
38
|
}
|
|
16
39
|
|
|
17
40
|
/**
|
|
@@ -67,7 +90,19 @@ export class WuManifest {
|
|
|
67
90
|
}
|
|
68
91
|
|
|
69
92
|
const manifestText = await response.text();
|
|
70
|
-
|
|
93
|
+
|
|
94
|
+
// 🔐 Validar tamaño del manifest
|
|
95
|
+
if (manifestText.length > this.security.maxManifestSize) {
|
|
96
|
+
throw new Error(`Manifest too large (${manifestText.length} bytes, max ${this.security.maxManifestSize})`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 🔐 Intentar parsear JSON de forma segura
|
|
100
|
+
let manifest;
|
|
101
|
+
try {
|
|
102
|
+
manifest = JSON.parse(manifestText);
|
|
103
|
+
} catch (parseError) {
|
|
104
|
+
throw new Error(`Invalid JSON in manifest: ${parseError.message}`);
|
|
105
|
+
}
|
|
71
106
|
|
|
72
107
|
// Validar manifest
|
|
73
108
|
const validatedManifest = this.validate(manifest);
|
|
@@ -133,13 +168,64 @@ export class WuManifest {
|
|
|
133
168
|
}
|
|
134
169
|
|
|
135
170
|
/**
|
|
136
|
-
*
|
|
171
|
+
* 🔐 SANITIZE STRING: Limpiar string de caracteres peligrosos
|
|
172
|
+
*/
|
|
173
|
+
_sanitizeString(str) {
|
|
174
|
+
if (typeof str !== 'string') return '';
|
|
175
|
+
return str
|
|
176
|
+
.replace(/[<>'"]/g, '') // Remove HTML chars
|
|
177
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control chars
|
|
178
|
+
.trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 🔐 CHECK DANGEROUS PATTERNS: Verificar patrones peligrosos
|
|
183
|
+
*/
|
|
184
|
+
_hasDangerousPatterns(str) {
|
|
185
|
+
if (typeof str !== 'string') return false;
|
|
186
|
+
return this.security.dangerousPatterns.some(pattern => pattern.test(str));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 🔐 VALIDATE URL: Verificar que URL es segura
|
|
191
|
+
*/
|
|
192
|
+
_isUrlSafe(url) {
|
|
193
|
+
if (typeof url !== 'string') return false;
|
|
194
|
+
|
|
195
|
+
// Verificar patrones peligrosos
|
|
196
|
+
if (this._hasDangerousPatterns(url)) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Verificar dominios bloqueados
|
|
201
|
+
try {
|
|
202
|
+
const urlObj = new URL(url, 'http://localhost');
|
|
203
|
+
if (this.security.blockedDomains.some(d => urlObj.hostname.includes(d))) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Si no es URL válida, verificar como path
|
|
208
|
+
if (this._hasDangerousPatterns(url)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validar manifest contra schema con validación de seguridad
|
|
137
218
|
* @param {Object} manifest - Manifest a validar
|
|
138
219
|
* @returns {Object} Manifest validado
|
|
139
220
|
*/
|
|
140
221
|
validate(manifest) {
|
|
141
222
|
const schema = this.schemas.get('wu.json');
|
|
142
223
|
|
|
224
|
+
// 🔐 Verificar que manifest es un objeto
|
|
225
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
226
|
+
throw new Error('Manifest must be a valid object');
|
|
227
|
+
}
|
|
228
|
+
|
|
143
229
|
// Verificar campos requeridos
|
|
144
230
|
for (const field of schema.required) {
|
|
145
231
|
if (!manifest[field]) {
|
|
@@ -147,23 +233,68 @@ export class WuManifest {
|
|
|
147
233
|
}
|
|
148
234
|
}
|
|
149
235
|
|
|
236
|
+
// 🔐 Validar nombre
|
|
237
|
+
if (typeof manifest.name !== 'string') {
|
|
238
|
+
throw new Error('name must be a string');
|
|
239
|
+
}
|
|
240
|
+
if (manifest.name.length > this.security.maxNameLength) {
|
|
241
|
+
throw new Error(`name too long (max ${this.security.maxNameLength} chars)`);
|
|
242
|
+
}
|
|
243
|
+
if (this._hasDangerousPatterns(manifest.name)) {
|
|
244
|
+
throw new Error('name contains dangerous patterns');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 🔐 Validar entry
|
|
248
|
+
if (typeof manifest.entry !== 'string') {
|
|
249
|
+
throw new Error('entry must be a string');
|
|
250
|
+
}
|
|
251
|
+
if (manifest.entry.length > this.security.maxEntryLength) {
|
|
252
|
+
throw new Error(`entry too long (max ${this.security.maxEntryLength} chars)`);
|
|
253
|
+
}
|
|
254
|
+
if (!this._isUrlSafe(manifest.entry)) {
|
|
255
|
+
throw new Error('entry contains dangerous patterns');
|
|
256
|
+
}
|
|
257
|
+
|
|
150
258
|
// Verificar tipos en sección wu
|
|
151
259
|
if (manifest.wu) {
|
|
152
260
|
const wu = manifest.wu;
|
|
153
|
-
const wuSchema = schema.wu;
|
|
154
261
|
|
|
155
262
|
if (wu.exports && typeof wu.exports !== 'object') {
|
|
156
263
|
throw new Error('wu.exports must be an object');
|
|
157
264
|
}
|
|
158
265
|
|
|
266
|
+
// 🔐 Validar límites de exports
|
|
267
|
+
if (wu.exports && Object.keys(wu.exports).length > this.security.maxExports) {
|
|
268
|
+
throw new Error(`Too many exports (max ${this.security.maxExports})`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 🔐 Validar cada export path
|
|
272
|
+
if (wu.exports) {
|
|
273
|
+
for (const [key, path] of Object.entries(wu.exports)) {
|
|
274
|
+
if (!this._isUrlSafe(path)) {
|
|
275
|
+
throw new Error(`Dangerous export path: ${key}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
159
280
|
if (wu.imports && !Array.isArray(wu.imports)) {
|
|
160
281
|
throw new Error('wu.imports must be an array');
|
|
161
282
|
}
|
|
162
283
|
|
|
284
|
+
// 🔐 Validar límites de imports
|
|
285
|
+
if (wu.imports && wu.imports.length > this.security.maxImports) {
|
|
286
|
+
throw new Error(`Too many imports (max ${this.security.maxImports})`);
|
|
287
|
+
}
|
|
288
|
+
|
|
163
289
|
if (wu.routes && !Array.isArray(wu.routes)) {
|
|
164
290
|
throw new Error('wu.routes must be an array');
|
|
165
291
|
}
|
|
166
292
|
|
|
293
|
+
// 🔐 Validar límites de routes
|
|
294
|
+
if (wu.routes && wu.routes.length > this.security.maxRoutes) {
|
|
295
|
+
throw new Error(`Too many routes (max ${this.security.maxRoutes})`);
|
|
296
|
+
}
|
|
297
|
+
|
|
167
298
|
if (wu.permissions && !Array.isArray(wu.permissions)) {
|
|
168
299
|
throw new Error('wu.permissions must be an array');
|
|
169
300
|
}
|
package/src/core/wu-plugin.js
CHANGED
|
@@ -1,53 +1,168 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🔌 WU-PLUGIN:
|
|
2
|
+
* 🔌 WU-PLUGIN: SECURE PLUGIN SYSTEM
|
|
3
3
|
*
|
|
4
|
-
* Sistema de plugins
|
|
4
|
+
* Sistema de plugins con sandboxing de seguridad
|
|
5
5
|
* - Plugin lifecycle (install, beforeMount, afterMount, etc.)
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
6
|
+
* - Sandboxed API (plugins no tienen acceso completo al core)
|
|
7
|
+
* - Permission system
|
|
8
|
+
* - Timeout protection
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export class WuPluginSystem {
|
|
11
12
|
constructor(core) {
|
|
12
|
-
this.
|
|
13
|
-
this.plugins = new Map();
|
|
14
|
-
this.hooks = new Map();
|
|
13
|
+
this._core = core; // Privado - no expuesto a plugins
|
|
14
|
+
this.plugins = new Map();
|
|
15
|
+
this.hooks = new Map();
|
|
15
16
|
|
|
16
17
|
// Hooks disponibles
|
|
17
18
|
this.availableHooks = [
|
|
18
|
-
'beforeInit',
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'beforeUnmount',
|
|
23
|
-
'afterUnmount',
|
|
24
|
-
'onError',
|
|
25
|
-
'onDestroy'
|
|
19
|
+
'beforeInit', 'afterInit',
|
|
20
|
+
'beforeMount', 'afterMount',
|
|
21
|
+
'beforeUnmount', 'afterUnmount',
|
|
22
|
+
'onError', 'onDestroy'
|
|
26
23
|
];
|
|
27
24
|
|
|
28
|
-
//
|
|
25
|
+
// 🔐 Permisos disponibles
|
|
26
|
+
this.availablePermissions = [
|
|
27
|
+
'mount', // Puede montar/desmontar apps
|
|
28
|
+
'events', // Puede emitir/escuchar eventos
|
|
29
|
+
'store', // Puede leer/escribir store
|
|
30
|
+
'apps', // Puede ver lista de apps
|
|
31
|
+
'config', // Puede modificar configuración
|
|
32
|
+
'unsafe' // Acceso completo (peligroso)
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// 🔐 Timeout para hooks (evita que plugins bloqueen)
|
|
36
|
+
this.hookTimeout = 5000; // 5 segundos
|
|
37
|
+
|
|
29
38
|
this.availableHooks.forEach(hook => {
|
|
30
39
|
this.hooks.set(hook, []);
|
|
31
40
|
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 🔐 CREATE SANDBOXED API: Crea API limitada para el plugin
|
|
45
|
+
* @param {Array} permissions - Permisos del plugin
|
|
46
|
+
* @returns {Object} API sandboxeada
|
|
47
|
+
*/
|
|
48
|
+
_createSandboxedApi(permissions) {
|
|
49
|
+
const api = {
|
|
50
|
+
// Info básica siempre disponible
|
|
51
|
+
version: this._core.version,
|
|
52
|
+
info: this._core.info,
|
|
53
|
+
|
|
54
|
+
// 📊 Métodos de solo lectura
|
|
55
|
+
getAppInfo: (appName) => {
|
|
56
|
+
const mounted = this._core.mounted.get(appName);
|
|
57
|
+
if (!mounted) return null;
|
|
58
|
+
return {
|
|
59
|
+
name: appName,
|
|
60
|
+
state: mounted.state,
|
|
61
|
+
timestamp: mounted.timestamp
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
getMountedApps: () => {
|
|
66
|
+
return Array.from(this._core.mounted.keys());
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
getStats: () => this._core.getStats()
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 🔐 Agregar métodos según permisos
|
|
73
|
+
if (permissions.includes('events') || permissions.includes('unsafe')) {
|
|
74
|
+
api.emit = (event, data) => this._core.eventBus.emit(event, data, { appName: 'plugin' });
|
|
75
|
+
api.on = (event, cb) => this._core.eventBus.on(event, cb);
|
|
76
|
+
api.off = (event, cb) => this._core.eventBus.off(event, cb);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (permissions.includes('store') || permissions.includes('unsafe')) {
|
|
80
|
+
api.getState = (path) => this._core.store.get(path);
|
|
81
|
+
api.setState = (path, value) => this._core.store.set(path, value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (permissions.includes('mount') || permissions.includes('unsafe')) {
|
|
85
|
+
api.mount = (appName, container) => this._core.mount(appName, container);
|
|
86
|
+
api.unmount = (appName) => this._core.unmount(appName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (permissions.includes('config') || permissions.includes('unsafe')) {
|
|
90
|
+
api.configure = (config) => {
|
|
91
|
+
// Solo permitir configuración segura
|
|
92
|
+
const safeKeys = ['debug', 'logLevel'];
|
|
93
|
+
const safeConfig = {};
|
|
94
|
+
for (const key of safeKeys) {
|
|
95
|
+
if (key in config) safeConfig[key] = config[key];
|
|
96
|
+
}
|
|
97
|
+
Object.assign(this._core, safeConfig);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 🚨 Acceso completo solo con permiso 'unsafe'
|
|
102
|
+
if (permissions.includes('unsafe')) {
|
|
103
|
+
api._unsafeCore = this._core;
|
|
104
|
+
console.warn('[WuPlugin] ⚠️ Plugin has unsafe access to core!');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Congelar API para evitar modificaciones
|
|
108
|
+
return Object.freeze(api);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 🔐 VALIDATE PLUGIN: Validar estructura del plugin
|
|
113
|
+
* @param {Object} plugin
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
_validatePlugin(plugin) {
|
|
117
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
118
|
+
throw new Error('[WuPlugin] Invalid plugin: must be an object');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!plugin.name || typeof plugin.name !== 'string') {
|
|
122
|
+
throw new Error('[WuPlugin] Invalid plugin: must have a name (string)');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (plugin.name.length > 50) {
|
|
126
|
+
throw new Error('[WuPlugin] Invalid plugin: name too long (max 50 chars)');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validar que los hooks sean funciones
|
|
130
|
+
for (const hookName of this.availableHooks) {
|
|
131
|
+
if (plugin[hookName] && typeof plugin[hookName] !== 'function') {
|
|
132
|
+
throw new Error(`[WuPlugin] Invalid plugin: ${hookName} must be a function`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validar permisos
|
|
137
|
+
if (plugin.permissions) {
|
|
138
|
+
if (!Array.isArray(plugin.permissions)) {
|
|
139
|
+
throw new Error('[WuPlugin] Invalid plugin: permissions must be an array');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const perm of plugin.permissions) {
|
|
143
|
+
if (!this.availablePermissions.includes(perm)) {
|
|
144
|
+
throw new Error(`[WuPlugin] Invalid permission: ${perm}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
32
148
|
|
|
33
|
-
|
|
149
|
+
return true;
|
|
34
150
|
}
|
|
35
151
|
|
|
36
152
|
/**
|
|
37
|
-
* 📦 USE: Instalar plugin
|
|
153
|
+
* 📦 USE: Instalar plugin con sandboxing
|
|
38
154
|
* @param {Object|Function} plugin - Plugin o factory function
|
|
39
155
|
* @param {Object} options - Opciones del plugin
|
|
40
156
|
*/
|
|
41
157
|
use(plugin, options = {}) {
|
|
42
158
|
// Si es una función, ejecutarla para obtener el plugin
|
|
159
|
+
// Nota: factory functions NO reciben acceso al core
|
|
43
160
|
if (typeof plugin === 'function') {
|
|
44
|
-
plugin = plugin(
|
|
161
|
+
plugin = plugin(options);
|
|
45
162
|
}
|
|
46
163
|
|
|
47
164
|
// Validar plugin
|
|
48
|
-
|
|
49
|
-
throw new Error('[WuPlugin] Plugin must have a name property');
|
|
50
|
-
}
|
|
165
|
+
this._validatePlugin(plugin);
|
|
51
166
|
|
|
52
167
|
// Verificar si ya está instalado
|
|
53
168
|
if (this.plugins.has(plugin.name)) {
|
|
@@ -55,15 +170,28 @@ export class WuPluginSystem {
|
|
|
55
170
|
return;
|
|
56
171
|
}
|
|
57
172
|
|
|
58
|
-
//
|
|
173
|
+
// Determinar permisos (por defecto: solo eventos)
|
|
174
|
+
const permissions = plugin.permissions || ['events'];
|
|
175
|
+
|
|
176
|
+
// 🔐 Crear API sandboxeada
|
|
177
|
+
const sandboxedApi = this._createSandboxedApi(permissions);
|
|
178
|
+
|
|
179
|
+
// Ejecutar install del plugin con API sandboxeada
|
|
59
180
|
if (plugin.install) {
|
|
60
|
-
|
|
181
|
+
try {
|
|
182
|
+
plugin.install(sandboxedApi, options);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(`[WuPlugin] Error installing "${plugin.name}":`, error);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
61
187
|
}
|
|
62
188
|
|
|
63
|
-
// Registrar hooks del plugin
|
|
189
|
+
// Registrar hooks del plugin con protección
|
|
64
190
|
this.availableHooks.forEach(hookName => {
|
|
65
191
|
if (typeof plugin[hookName] === 'function') {
|
|
66
|
-
|
|
192
|
+
// Wrap el hook con timeout y try-catch
|
|
193
|
+
const wrappedHook = this._wrapHook(plugin[hookName].bind(plugin), plugin.name, hookName);
|
|
194
|
+
this.registerHook(hookName, wrappedHook);
|
|
67
195
|
}
|
|
68
196
|
});
|
|
69
197
|
|
|
@@ -71,31 +199,52 @@ export class WuPluginSystem {
|
|
|
71
199
|
this.plugins.set(plugin.name, {
|
|
72
200
|
plugin,
|
|
73
201
|
options,
|
|
202
|
+
permissions,
|
|
203
|
+
sandboxedApi,
|
|
74
204
|
installedAt: Date.now()
|
|
75
205
|
});
|
|
76
206
|
|
|
77
|
-
console.log(`[WuPlugin] ✅ Plugin "${plugin.name}" installed`);
|
|
207
|
+
console.log(`[WuPlugin] ✅ Plugin "${plugin.name}" installed (permissions: ${permissions.join(', ')})`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 🔐 WRAP HOOK: Envolver hook con timeout y error handling
|
|
212
|
+
*/
|
|
213
|
+
_wrapHook(hookFn, pluginName, hookName) {
|
|
214
|
+
return async (context) => {
|
|
215
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
reject(new Error(`Plugin "${pluginName}" hook "${hookName}" timed out after ${this.hookTimeout}ms`));
|
|
218
|
+
}, this.hookTimeout);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
// Race entre el hook y el timeout
|
|
223
|
+
return await Promise.race([
|
|
224
|
+
hookFn(context),
|
|
225
|
+
timeoutPromise
|
|
226
|
+
]);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(`[WuPlugin] Error in ${pluginName}.${hookName}:`, error);
|
|
229
|
+
// No propagar error para no romper otros plugins
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
78
233
|
}
|
|
79
234
|
|
|
80
235
|
/**
|
|
81
|
-
* 🪝 REGISTER HOOK
|
|
82
|
-
* @param {string} hookName - Nombre del hook
|
|
83
|
-
* @param {Function} callback - Función callback
|
|
236
|
+
* 🪝 REGISTER HOOK
|
|
84
237
|
*/
|
|
85
238
|
registerHook(hookName, callback) {
|
|
86
239
|
if (!this.hooks.has(hookName)) {
|
|
87
240
|
console.warn(`[WuPlugin] Unknown hook: ${hookName}`);
|
|
88
241
|
return;
|
|
89
242
|
}
|
|
90
|
-
|
|
91
243
|
this.hooks.get(hookName).push(callback);
|
|
92
244
|
}
|
|
93
245
|
|
|
94
246
|
/**
|
|
95
|
-
* 🎯 CALL HOOK
|
|
96
|
-
* @param {string} hookName - Nombre del hook
|
|
97
|
-
* @param {*} context - Contexto a pasar a los callbacks
|
|
98
|
-
* @returns {Promise<boolean>} false si algún callback cancela la operación
|
|
247
|
+
* 🎯 CALL HOOK
|
|
99
248
|
*/
|
|
100
249
|
async callHook(hookName, context) {
|
|
101
250
|
const callbacks = this.hooks.get(hookName) || [];
|
|
@@ -103,17 +252,11 @@ export class WuPluginSystem {
|
|
|
103
252
|
for (const callback of callbacks) {
|
|
104
253
|
try {
|
|
105
254
|
const result = await callback(context);
|
|
106
|
-
|
|
107
|
-
// Si retorna false, cancelar operación
|
|
108
255
|
if (result === false) {
|
|
109
|
-
console.log(`[WuPlugin] Hook ${hookName} cancelled by plugin`);
|
|
110
256
|
return false;
|
|
111
257
|
}
|
|
112
258
|
} catch (error) {
|
|
113
259
|
console.error(`[WuPlugin] Error in hook ${hookName}:`, error);
|
|
114
|
-
|
|
115
|
-
// Llamar hook de error
|
|
116
|
-
await this.callHook('onError', { hookName, error, context });
|
|
117
260
|
}
|
|
118
261
|
}
|
|
119
262
|
|
|
@@ -121,61 +264,46 @@ export class WuPluginSystem {
|
|
|
121
264
|
}
|
|
122
265
|
|
|
123
266
|
/**
|
|
124
|
-
* 🗑️ UNINSTALL
|
|
125
|
-
* @param {string} pluginName - Nombre del plugin
|
|
267
|
+
* 🗑️ UNINSTALL
|
|
126
268
|
*/
|
|
127
269
|
uninstall(pluginName) {
|
|
128
270
|
const pluginData = this.plugins.get(pluginName);
|
|
129
|
-
|
|
130
271
|
if (!pluginData) {
|
|
131
272
|
console.warn(`[WuPlugin] Plugin "${pluginName}" not found`);
|
|
132
273
|
return;
|
|
133
274
|
}
|
|
134
275
|
|
|
135
|
-
const { plugin } = pluginData;
|
|
276
|
+
const { plugin, sandboxedApi } = pluginData;
|
|
136
277
|
|
|
137
|
-
// Ejecutar uninstall si existe
|
|
138
278
|
if (plugin.uninstall) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.availableHooks.forEach(hookName => {
|
|
144
|
-
if (typeof plugin[hookName] === 'function') {
|
|
145
|
-
const callbacks = this.hooks.get(hookName);
|
|
146
|
-
const index = callbacks.indexOf(plugin[hookName]);
|
|
147
|
-
if (index > -1) {
|
|
148
|
-
callbacks.splice(index, 1);
|
|
149
|
-
}
|
|
279
|
+
try {
|
|
280
|
+
plugin.uninstall(sandboxedApi);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(`[WuPlugin] Error uninstalling "${pluginName}":`, error);
|
|
150
283
|
}
|
|
151
|
-
}
|
|
284
|
+
}
|
|
152
285
|
|
|
153
|
-
// Remover plugin
|
|
154
286
|
this.plugins.delete(pluginName);
|
|
155
|
-
|
|
156
287
|
console.log(`[WuPlugin] ✅ Plugin "${pluginName}" uninstalled`);
|
|
157
288
|
}
|
|
158
289
|
|
|
159
290
|
/**
|
|
160
|
-
* 📋 GET PLUGIN
|
|
161
|
-
* @param {string} pluginName - Nombre del plugin
|
|
162
|
-
* @returns {Object}
|
|
291
|
+
* 📋 GET PLUGIN
|
|
163
292
|
*/
|
|
164
293
|
getPlugin(pluginName) {
|
|
165
294
|
return this.plugins.get(pluginName)?.plugin;
|
|
166
295
|
}
|
|
167
296
|
|
|
168
297
|
/**
|
|
169
|
-
* 📊 GET STATS
|
|
170
|
-
* @returns {Object}
|
|
298
|
+
* 📊 GET STATS
|
|
171
299
|
*/
|
|
172
300
|
getStats() {
|
|
173
301
|
return {
|
|
174
302
|
totalPlugins: this.plugins.size,
|
|
175
303
|
plugins: Array.from(this.plugins.entries()).map(([name, data]) => ({
|
|
176
304
|
name,
|
|
177
|
-
|
|
178
|
-
|
|
305
|
+
permissions: data.permissions,
|
|
306
|
+
installedAt: data.installedAt
|
|
179
307
|
})),
|
|
180
308
|
hooks: Array.from(this.hooks.entries()).map(([name, callbacks]) => ({
|
|
181
309
|
name,
|
|
@@ -185,23 +313,25 @@ export class WuPluginSystem {
|
|
|
185
313
|
}
|
|
186
314
|
|
|
187
315
|
/**
|
|
188
|
-
* 🧹 CLEANUP
|
|
316
|
+
* 🧹 CLEANUP
|
|
189
317
|
*/
|
|
190
318
|
cleanup() {
|
|
191
319
|
for (const [name] of this.plugins) {
|
|
192
320
|
this.uninstall(name);
|
|
193
321
|
}
|
|
194
|
-
|
|
195
|
-
console.log('[WuPlugin] 🧹 All plugins cleaned up');
|
|
196
322
|
}
|
|
197
323
|
}
|
|
198
324
|
|
|
199
325
|
/**
|
|
200
|
-
* 📦 PLUGIN HELPER: Helper para crear plugins
|
|
326
|
+
* 📦 PLUGIN HELPER: Helper para crear plugins
|
|
327
|
+
* @param {Object} config - Configuración del plugin
|
|
328
|
+
* @param {string} config.name - Nombre del plugin
|
|
329
|
+
* @param {Array} config.permissions - Permisos requeridos
|
|
201
330
|
*/
|
|
202
331
|
export const createPlugin = (config) => {
|
|
203
332
|
return {
|
|
204
333
|
name: config.name,
|
|
334
|
+
permissions: config.permissions || ['events'],
|
|
205
335
|
install: config.install,
|
|
206
336
|
uninstall: config.uninstall,
|
|
207
337
|
beforeMount: config.beforeMount,
|
package/src/index.js
CHANGED
|
@@ -103,7 +103,7 @@ if (typeof window !== 'undefined') {
|
|
|
103
103
|
|
|
104
104
|
// Configurar propiedades si no existen
|
|
105
105
|
if (!wu.version) {
|
|
106
|
-
wu.version = '1.0.
|
|
106
|
+
wu.version = '1.0.6';
|
|
107
107
|
console.log('🚀 Wu Framework loaded - Universal Microfrontends ready');
|
|
108
108
|
wu.info = {
|
|
109
109
|
name: 'Wu Framework',
|