wu-framework 1.0.5 → 1.1.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.
@@ -1,46 +1,156 @@
1
1
  /**
2
- * 📡 WU-EVENT-BUS: ADVANCED PUB/SUB SYSTEM
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
- * - Performance optimizado
9
+ * - Verificación de apps autorizadas
10
10
  */
11
11
 
12
12
  export class WuEventBus {
13
13
  constructor() {
14
- this.listeners = new Map(); // eventName -> Set<callback>
15
- this.history = []; // Event history para replay
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
- console.log('[WuEventBus] 📡 Advanced event bus initialized');
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
- * 📢 EMIT: Emitir evento
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 - Opciones { appName, timestamp, meta }
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: options.appName || 'unknown',
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: Notificar listeners con wildcards
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: Verificar si evento coincide con patrón 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: Agregar evento al historial
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: Reproducir eventos del historial
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: Limpiar historial de eventos
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: Obtener estadísticas
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: Configurar event bus
240
- * @param {Object} config - Nueva configuración
319
+ * ⚙️ CONFIGURE
241
320
  */
242
321
  configure(config) {
243
- this.config = {
244
- ...this.config,
245
- ...config
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: Remover todos los listeners
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
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * 📋 WU-MANIFEST: SISTEMA DE MANIFIESTOS SIMPLIFICADO
3
- * Maneja wu.json sin complicaciones innecesarias
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
- // Definir schema básico
12
- this.defineSchema();
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
- console.log('[WuManifest] 📋 Manifest system initialized');
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
- const manifest = JSON.parse(manifestText);
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
- * Validar manifest contra schema
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
  }