wu-framework 1.1.16 → 1.1.17

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.
@@ -24,19 +24,27 @@ class WuLogger {
24
24
  * Detectar si estamos en desarrollo
25
25
  */
26
26
  detectEnvironment() {
27
- // Múltiples formas de detectar desarrollo
28
- return (
29
- // Vite development
30
- window.location.hostname === 'localhost' ||
31
- window.location.hostname === '127.0.0.1' ||
32
- window.location.port !== '' ||
33
- // NODE_ENV si está disponible
34
- (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') ||
35
- // URL params para forzar debug
36
- new URLSearchParams(window.location.search).has('wu-debug') ||
37
- // Manual override
38
- window.WU_DEBUG === true
39
- );
27
+ // 1. Explicit flag takes priority
28
+ if (typeof window !== 'undefined' && window.WU_DEBUG === true) return true;
29
+ if (typeof window !== 'undefined' && window.WU_DEBUG === false) return false;
30
+
31
+ // 2. NODE_ENV check (works in bundlers and Node)
32
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return false;
33
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') return true;
34
+
35
+ // 3. Browser heuristics (only if window exists)
36
+ if (typeof window !== 'undefined' && window.location) {
37
+ const hostname = window.location.hostname;
38
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]') return true;
39
+
40
+ // URL param override
41
+ try {
42
+ if (new URLSearchParams(window.location.search).has('wu-debug')) return true;
43
+ } catch {}
44
+ }
45
+
46
+ // 4. Default: assume production
47
+ return false;
40
48
  }
41
49
 
42
50
  /**
@@ -136,69 +144,178 @@ var wuLogger = /*#__PURE__*/Object.freeze({
136
144
  });
137
145
 
138
146
  /**
139
- * 🚀 WU-LOADER: SISTEMA DE CARGA DINÁMICA UNIVERSAL
147
+ * WU-LOADER: SISTEMA DE CARGA DINAMICA UNIVERSAL
140
148
  * Carga aplicaciones y componentes sin depender del framework
149
+ *
150
+ * Cache strategy: LRU with TTL eviction.
151
+ * Entries track lastAccess time. When the cache reaches maxCacheSize,
152
+ * the least recently accessed entry is evicted. Entries older than
153
+ * cacheTTL are treated as stale and removed on access or eviction.
141
154
  */
142
155
 
143
156
 
157
+ /**
158
+ * @typedef {Object} WuLoaderOptions
159
+ * @property {number} [maxCacheSize=50] - Maximum cache entries
160
+ * @property {number} [cacheTTL=1800000] - Cache TTL in ms (default 30min)
161
+ */
162
+
163
+ /**
164
+ * @typedef {Object} WuLoaderStats
165
+ * @property {number} cached - Number of cached entries
166
+ * @property {number} loading - Number of in-flight loads
167
+ * @property {number} maxCacheSize - Max cache size setting
168
+ * @property {number} cacheTTL - Cache TTL setting
169
+ * @property {string[]} cacheKeys - Cached URL keys
170
+ */
171
+
144
172
  class WuLoader {
145
- constructor() {
173
+ /**
174
+ * @param {Object} options
175
+ * @param {number} [options.maxCacheSize=50] - Maximum number of entries in the cache
176
+ * @param {number} [options.cacheTTL=1800000] - Time-to-live for cache entries in ms (default 30 minutes)
177
+ */
178
+ constructor(options = {}) {
179
+ this.maxCacheSize = options.maxCacheSize ?? 50;
180
+ this.cacheTTL = options.cacheTTL ?? 1800000;
146
181
  this.cache = new Map();
147
182
  this.loadingPromises = new Map();
148
183
 
149
- logger.debug('[WuLoader] 📦 Dynamic loader initialized');
184
+ logger.debug('[WuLoader] Dynamic loader initialized');
150
185
  }
151
186
 
152
187
  /**
153
- * Cargar aplicación completa
154
- * @param {string} appUrl - URL base de la aplicación
155
- * @param {Object} manifest - Manifest de la aplicación
156
- * @returns {string} Código JavaScript de la aplicación
188
+ * Read from cache with TTL validation and LRU access tracking.
189
+ * Returns undefined if the entry does not exist or has expired.
190
+ * @param {string} key
191
+ * @returns {string|undefined}
192
+ */
193
+ _cacheGet(key) {
194
+ if (!this.cache.has(key)) {
195
+ return undefined;
196
+ }
197
+
198
+ const entry = this.cache.get(key);
199
+ const now = Date.now();
200
+
201
+ if (now - entry.timestamp > this.cacheTTL) {
202
+ this.cache.delete(key);
203
+ logger.debug(`[WuLoader] Cache expired for: ${key}`);
204
+ return undefined;
205
+ }
206
+
207
+ // Promote: delete and re-insert so iteration order reflects recency.
208
+ // Map iteration order in JS follows insertion order, so the oldest
209
+ // inserted entry is always first -- exactly what we need for LRU eviction.
210
+ this.cache.delete(key);
211
+ entry.lastAccess = now;
212
+ this.cache.set(key, entry);
213
+
214
+ return entry.code;
215
+ }
216
+
217
+ /**
218
+ * Write to cache. Evicts stale and LRU entries before inserting.
219
+ * @param {string} key
220
+ * @param {string} code
221
+ */
222
+ _cacheSet(key, code) {
223
+ // If the key already exists, remove it first so re-insertion
224
+ // moves it to the end (most-recently-used position).
225
+ if (this.cache.has(key)) {
226
+ this.cache.delete(key);
227
+ }
228
+
229
+ this._evictIfNeeded();
230
+
231
+ const now = Date.now();
232
+ this.cache.set(key, {
233
+ code,
234
+ timestamp: now,
235
+ lastAccess: now
236
+ });
237
+ }
238
+
239
+ /**
240
+ * Evict entries until cache is below maxCacheSize.
241
+ *
242
+ * Two-pass strategy:
243
+ * 1. Remove all expired entries (TTL exceeded).
244
+ * 2. If still at capacity, remove the least recently accessed entry.
245
+ * Because Map preserves insertion order and _cacheGet promotes on
246
+ * access, the first key from the iterator is always the LRU entry.
247
+ */
248
+ _evictIfNeeded() {
249
+ const now = Date.now();
250
+
251
+ // Pass 1: purge expired entries
252
+ for (const [key, entry] of this.cache) {
253
+ if (now - entry.timestamp > this.cacheTTL) {
254
+ this.cache.delete(key);
255
+ logger.debug(`[WuLoader] Evicted expired entry: ${key}`);
256
+ }
257
+ }
258
+
259
+ // Pass 2: evict LRU entries until we are under the limit
260
+ while (this.cache.size >= this.maxCacheSize) {
261
+ // Map.keys().next() gives us the oldest-inserted key (LRU)
262
+ const oldestKey = this.cache.keys().next().value;
263
+ this.cache.delete(oldestKey);
264
+ logger.debug(`[WuLoader] Evicted LRU entry: ${oldestKey}`);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Cargar aplicacion completa
270
+ * @param {string} appUrl - URL base de la aplicacion
271
+ * @param {Object} manifest - Manifest de la aplicacion
272
+ * @returns {string} Codigo JavaScript de la aplicacion
157
273
  */
158
274
  async loadApp(appUrl, manifest) {
159
275
  const entryFile = manifest?.entry || 'index.js';
160
276
  const fullUrl = `${appUrl}/${entryFile}`;
161
277
 
162
- logger.debug(`[WuLoader] 📥 Loading app from: ${fullUrl}`);
278
+ logger.debug(`[WuLoader] Loading app from: ${fullUrl}`);
163
279
 
164
280
  try {
165
- // Verificar cache
166
- if (this.cache.has(fullUrl)) {
167
- logger.debug(`[WuLoader] Cache hit for: ${fullUrl}`);
168
- return this.cache.get(fullUrl);
281
+ // Check cache with TTL and LRU tracking
282
+ const cached = this._cacheGet(fullUrl);
283
+ if (cached !== undefined) {
284
+ logger.debug(`[WuLoader] Cache hit for: ${fullUrl}`);
285
+ return cached;
169
286
  }
170
287
 
171
- // Verificar si ya está cargando
288
+ // Check if already loading
172
289
  if (this.loadingPromises.has(fullUrl)) {
173
- logger.debug(`[WuLoader] Loading in progress for: ${fullUrl}`);
290
+ logger.debug(`[WuLoader] Loading in progress for: ${fullUrl}`);
174
291
  return await this.loadingPromises.get(fullUrl);
175
292
  }
176
293
 
177
- // Crear promesa de carga
294
+ // Create loading promise
178
295
  const loadingPromise = this.fetchCode(fullUrl);
179
296
  this.loadingPromises.set(fullUrl, loadingPromise);
180
297
 
181
298
  const code = await loadingPromise;
182
299
 
183
- // Limpiar promesa de carga y cachear resultado
300
+ // Clean up loading promise and cache result
184
301
  this.loadingPromises.delete(fullUrl);
185
- this.cache.set(fullUrl, code);
302
+ this._cacheSet(fullUrl, code);
186
303
 
187
- logger.debug(`[WuLoader] App loaded successfully: ${fullUrl}`);
304
+ logger.debug(`[WuLoader] App loaded successfully: ${fullUrl}`);
188
305
  return code;
189
306
 
190
307
  } catch (error) {
191
308
  this.loadingPromises.delete(fullUrl);
192
- console.error(`[WuLoader] Failed to load app: ${fullUrl}`, error);
309
+ console.error(`[WuLoader] Failed to load app: ${fullUrl}`, error);
193
310
  throw new Error(`Failed to load app from ${fullUrl}: ${error.message}`);
194
311
  }
195
312
  }
196
313
 
197
314
  /**
198
- * Cargar componente específico
199
- * @param {string} appUrl - URL base de la aplicación
315
+ * Cargar componente especifico
316
+ * @param {string} appUrl - URL base de la aplicacion
200
317
  * @param {string} componentPath - Ruta del componente
201
- * @returns {Function} Función del componente
318
+ * @returns {Function} Funcion del componente
202
319
  */
203
320
  async loadComponent(appUrl, componentPath) {
204
321
  // Normalizar ruta del componente
@@ -212,13 +329,13 @@ class WuLoader {
212
329
 
213
330
  const fullUrl = `${appUrl}/${normalizedPath}`;
214
331
 
215
- logger.debug(`[WuLoader] 🧩 Loading component from: ${fullUrl}`);
332
+ logger.debug(`[WuLoader] Loading component from: ${fullUrl}`);
216
333
 
217
334
  try {
218
- // Cargar código del componente
335
+ // Cargar codigo del componente
219
336
  const code = await this.loadCode(fullUrl);
220
337
 
221
- // Crear función que retorna el componente
338
+ // Crear funcion que retorna el componente
222
339
  const componentFunction = new Function('require', 'module', 'exports', `
223
340
  ${code}
224
341
  return typeof module.exports === 'function' ? module.exports :
@@ -235,39 +352,40 @@ class WuLoader {
235
352
 
236
353
  const component = componentFunction(fakeRequire, fakeModule, fakeModule.exports);
237
354
 
238
- logger.debug(`[WuLoader] Component loaded: ${componentPath}`);
355
+ logger.debug(`[WuLoader] Component loaded: ${componentPath}`);
239
356
  return component;
240
357
 
241
358
  } catch (error) {
242
- console.error(`[WuLoader] Failed to load component: ${componentPath}`, error);
359
+ console.error(`[WuLoader] Failed to load component: ${componentPath}`, error);
243
360
  throw new Error(`Failed to load component ${componentPath}: ${error.message}`);
244
361
  }
245
362
  }
246
363
 
247
364
  /**
248
- * Cargar código con cache
365
+ * Cargar codigo con cache
249
366
  * @param {string} url - URL del archivo
250
- * @returns {string} Código JavaScript
367
+ * @returns {string} Codigo JavaScript
251
368
  */
252
369
  async loadCode(url) {
253
- // Verificar cache
254
- if (this.cache.has(url)) {
255
- return this.cache.get(url);
370
+ // Check cache with TTL and LRU tracking
371
+ const cached = this._cacheGet(url);
372
+ if (cached !== undefined) {
373
+ return cached;
256
374
  }
257
375
 
258
- // Verificar si ya está cargando
376
+ // Check if already loading
259
377
  if (this.loadingPromises.has(url)) {
260
378
  return await this.loadingPromises.get(url);
261
379
  }
262
380
 
263
- // Crear promesa de carga
381
+ // Create loading promise
264
382
  const loadingPromise = this.fetchCode(url);
265
383
  this.loadingPromises.set(url, loadingPromise);
266
384
 
267
385
  try {
268
386
  const code = await loadingPromise;
269
387
  this.loadingPromises.delete(url);
270
- this.cache.set(url, code);
388
+ this._cacheSet(url, code);
271
389
  return code;
272
390
  } catch (error) {
273
391
  this.loadingPromises.delete(url);
@@ -276,9 +394,9 @@ class WuLoader {
276
394
  }
277
395
 
278
396
  /**
279
- * Realizar fetch del código
397
+ * Realizar fetch del codigo
280
398
  * @param {string} url - URL del archivo
281
- * @returns {string} Código JavaScript
399
+ * @returns {string} Codigo JavaScript
282
400
  */
283
401
  async fetchCode(url) {
284
402
  const response = await fetch(url, {
@@ -306,25 +424,25 @@ class WuLoader {
306
424
  * @param {Array} appConfigs - Configuraciones de aplicaciones
307
425
  */
308
426
  async preload(appConfigs) {
309
- logger.debug(`[WuLoader] 🚀 Preloading ${appConfigs.length} apps...`);
427
+ logger.debug(`[WuLoader] Preloading ${appConfigs.length} apps...`);
310
428
 
311
429
  const preloadPromises = appConfigs.map(async (config) => {
312
430
  try {
313
431
  await this.loadApp(config.url, config.manifest);
314
- logger.debug(`[WuLoader] Preloaded: ${config.name}`);
432
+ logger.debug(`[WuLoader] Preloaded: ${config.name}`);
315
433
  } catch (error) {
316
- logger.warn(`[WuLoader] ⚠️ Failed to preload ${config.name}:`, error.message);
434
+ logger.warn(`[WuLoader] Failed to preload ${config.name}:`, error.message);
317
435
  }
318
436
  });
319
437
 
320
438
  await Promise.allSettled(preloadPromises);
321
- logger.debug(`[WuLoader] 🎉 Preload completed`);
439
+ logger.debug(`[WuLoader] Preload completed`);
322
440
  }
323
441
 
324
442
  /**
325
- * Verificar si una URL está disponible
443
+ * Verificar si una URL esta disponible
326
444
  * @param {string} url - URL a verificar
327
- * @returns {boolean} True si está disponible
445
+ * @returns {boolean} True si esta disponible
328
446
  */
329
447
  async isAvailable(url) {
330
448
  try {
@@ -368,9 +486,9 @@ class WuLoader {
368
486
  try {
369
487
  const component = await this.loadComponent(app.url, exportPath);
370
488
  resolved.set(importPath, component);
371
- logger.debug(`[WuLoader] Resolved dependency: ${importPath}`);
489
+ logger.debug(`[WuLoader] Resolved dependency: ${importPath}`);
372
490
  } catch (error) {
373
- console.error(`[WuLoader] Failed to resolve: ${importPath}`, error);
491
+ console.error(`[WuLoader] Failed to resolve: ${importPath}`, error);
374
492
  }
375
493
  }
376
494
 
@@ -379,7 +497,7 @@ class WuLoader {
379
497
 
380
498
  /**
381
499
  * Limpiar cache
382
- * @param {string} pattern - Patrón opcional para limpiar URLs específicas
500
+ * @param {string} pattern - Patron opcional para limpiar URLs especificas
383
501
  */
384
502
  clearCache(pattern) {
385
503
  if (pattern) {
@@ -387,21 +505,23 @@ class WuLoader {
387
505
  for (const [url] of this.cache) {
388
506
  if (regex.test(url)) {
389
507
  this.cache.delete(url);
390
- logger.debug(`[WuLoader] 🗑️ Cleared cache for: ${url}`);
508
+ logger.debug(`[WuLoader] Cleared cache for: ${url}`);
391
509
  }
392
510
  }
393
511
  } else {
394
512
  this.cache.clear();
395
- logger.debug(`[WuLoader] 🗑️ Cache cleared completely`);
513
+ logger.debug(`[WuLoader] Cache cleared completely`);
396
514
  }
397
515
  }
398
516
 
399
517
  /**
400
- * Obtener estadísticas del loader
518
+ * Obtener estadisticas del loader
401
519
  */
402
520
  getStats() {
403
521
  return {
404
522
  cached: this.cache.size,
523
+ maxCacheSize: this.maxCacheSize,
524
+ cacheTTL: this.cacheTTL,
405
525
  loading: this.loadingPromises.size,
406
526
  cacheKeys: Array.from(this.cache.keys())
407
527
  };
@@ -899,8 +1019,9 @@ class WuStyleBridge {
899
1019
 
900
1020
 
901
1021
  class WuProxySandbox {
902
- constructor(appName) {
1022
+ constructor(appName, options = {}) {
903
1023
  this.appName = appName;
1024
+ this.options = options;
904
1025
  this.proxy = null;
905
1026
  this.fakeWindow = Object.create(null);
906
1027
  this.active = false;
@@ -2667,6 +2788,29 @@ class WuManifest {
2667
2788
  }
2668
2789
  }
2669
2790
 
2791
+ // Validate optional fields
2792
+ if (manifest.styleMode !== undefined) {
2793
+ const validModes = ['shared', 'isolated', 'fully-isolated'];
2794
+ if (!validModes.includes(manifest.styleMode)) {
2795
+ logger.warn(`[WuManifest] Invalid styleMode "${manifest.styleMode}", defaulting to "shared". Valid: ${validModes.join(', ')}`);
2796
+ manifest.styleMode = 'shared';
2797
+ }
2798
+ }
2799
+
2800
+ if (manifest.version !== undefined && typeof manifest.version !== 'string') {
2801
+ logger.warn('[WuManifest] version must be a string, ignoring');
2802
+ delete manifest.version;
2803
+ }
2804
+
2805
+ if (manifest.folder !== undefined) {
2806
+ if (typeof manifest.folder !== 'string') {
2807
+ logger.warn('[WuManifest] folder must be a string, ignoring');
2808
+ delete manifest.folder;
2809
+ } else if (this._hasDangerousPatterns(manifest.folder)) {
2810
+ throw new Error('folder contains dangerous patterns');
2811
+ }
2812
+ }
2813
+
2670
2814
  // Normalizar y limpiar manifest
2671
2815
  return this.normalize(manifest);
2672
2816
  }
@@ -2884,6 +3028,15 @@ class WuManifest {
2884
3028
  * - API minimalista: get(), set(), on()
2885
3029
  */
2886
3030
 
3031
+ /**
3032
+ * @typedef {Object} WuStoreMetrics
3033
+ * @property {number} reads - Total read operations
3034
+ * @property {number} writes - Total write operations
3035
+ * @property {number} notifications - Total notifications sent
3036
+ * @property {number} bufferUtilization - Ring buffer utilization (0-1)
3037
+ * @property {number} listenerCount - Active listener count
3038
+ */
3039
+
2887
3040
  class WuStore {
2888
3041
  constructor(bufferSize = 256) {
2889
3042
  // Ring Buffer configuration
@@ -2939,8 +3092,9 @@ class WuStore {
2939
3092
  set(path, value) {
2940
3093
  this.metrics.writes++;
2941
3094
 
2942
- // Write to ring buffer (lock-free)
2943
- const sequence = this.cursor++;
3095
+ // Write to ring buffer (lock-free, wraps at buffer boundary)
3096
+ const sequence = this.cursor;
3097
+ this.cursor = (this.cursor + 1) % (this.bufferSize * this.bufferSize);
2944
3098
  const index = sequence & this.mask;
2945
3099
 
2946
3100
  // Reuse buffer slot (zero allocation)
@@ -3027,7 +3181,7 @@ class WuStore {
3027
3181
  getMetrics() {
3028
3182
  return {
3029
3183
  ...this.metrics,
3030
- bufferUtilization: (this.cursor % this.bufferSize) / this.bufferSize,
3184
+ bufferUtilization: Math.min(1, this.cursor / this.bufferSize),
3031
3185
  listenerCount: this.listeners.size + this.patternListeners.size
3032
3186
  };
3033
3187
  }
@@ -3438,6 +3592,9 @@ class WuCache {
3438
3592
  cooldownUntil: 0
3439
3593
  };
3440
3594
 
3595
+ // Rate limit notification flag (log only once per cooldown)
3596
+ this._rateLimitNotified = false;
3597
+
3441
3598
  // Memory cache
3442
3599
  this.memoryCache = new Map();
3443
3600
 
@@ -3472,6 +3629,7 @@ class WuCache {
3472
3629
  }
3473
3630
  // Cooldown terminado
3474
3631
  this.rateLimiting.inCooldown = false;
3632
+ this._rateLimitNotified = false;
3475
3633
  this.rateLimiting.operations = [];
3476
3634
  }
3477
3635
 
@@ -3497,7 +3655,22 @@ class WuCache {
3497
3655
  }
3498
3656
 
3499
3657
  /**
3500
- * 🔐 GET RATE LIMIT STATUS
3658
+ * Handle rate-limited operations: log once per cooldown, return diagnostic object
3659
+ * @param {string} operation - The operation that was rate limited ('get' or 'set')
3660
+ * @param {string} key - The cache key that was rejected
3661
+ * @returns {{ rateLimited: true, operation: string, key: string }}
3662
+ */
3663
+ _onRateLimited(operation, key) {
3664
+ if (!this._rateLimitNotified) {
3665
+ const cooldownRemaining = Math.max(0, this.rateLimiting.cooldownUntil - Date.now());
3666
+ logger.warn(`[WuCache] Rate limited: ${operation} for key "${key}" rejected. ${cooldownRemaining}ms remaining in cooldown.`);
3667
+ this._rateLimitNotified = true;
3668
+ }
3669
+ return { rateLimited: true, operation, key };
3670
+ }
3671
+
3672
+ /**
3673
+ * GET RATE LIMIT STATUS
3501
3674
  */
3502
3675
  getRateLimitStatus() {
3503
3676
  const now = Date.now();
@@ -3521,7 +3694,8 @@ class WuCache {
3521
3694
  get(key) {
3522
3695
  // 🔐 Check rate limit
3523
3696
  if (!this._checkRateLimit()) {
3524
- return null; // Silently fail on rate limit
3697
+ this._onRateLimited('get', key);
3698
+ return null;
3525
3699
  }
3526
3700
 
3527
3701
  // 1. Buscar en memoria
@@ -3568,7 +3742,8 @@ class WuCache {
3568
3742
  set(key, value, ttl) {
3569
3743
  // 🔐 Check rate limit
3570
3744
  if (!this._checkRateLimit()) {
3571
- return false; // Reject on rate limit
3745
+ this._onRateLimited('set', key);
3746
+ return false;
3572
3747
  }
3573
3748
 
3574
3749
  try {
@@ -3893,6 +4068,25 @@ class WuCache {
3893
4068
  * - Verificación de apps autorizadas
3894
4069
  */
3895
4070
 
4071
+ /**
4072
+ * @typedef {Object} WuEvent
4073
+ * @property {string} name - Event name
4074
+ * @property {*} data - Event payload
4075
+ * @property {number} timestamp - Event timestamp
4076
+ * @property {string} appName - Source app name
4077
+ * @property {Object} meta - Additional metadata
4078
+ * @property {boolean} verified - Whether origin was verified
4079
+ */
4080
+
4081
+ /**
4082
+ * @typedef {Object} WuEventBusConfig
4083
+ * @property {number} [maxHistory=100] - Maximum events in history
4084
+ * @property {boolean} [enableReplay=true] - Enable event replay
4085
+ * @property {boolean} [enableWildcards=true] - Enable wildcard matching
4086
+ * @property {boolean} [logEvents=false] - Log all events
4087
+ * @property {boolean} [strictMode=false] - Reject unauthorized events
4088
+ * @property {boolean} [validateOrigin=true] - Validate event origins
4089
+ */
3896
4090
 
3897
4091
  class WuEventBus {
3898
4092
  constructor() {
@@ -3903,16 +4097,21 @@ class WuEventBus {
3903
4097
  this.authorizedApps = new Map(); // appName -> { token, permissions }
3904
4098
  this.trustedEvents = new Set(['wu:*', 'system:*']); // Eventos del sistema
3905
4099
 
4100
+ // Auto-detect production environment for strictMode default
4101
+ const isProduction = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';
4102
+
3906
4103
  this.config = {
3907
4104
  maxHistory: 100,
3908
4105
  enableReplay: true,
3909
4106
  enableWildcards: true,
3910
4107
  logEvents: false,
3911
4108
  // 🔐 Opciones de seguridad
3912
- strictMode: false, // Si true, rechaza eventos de apps no autorizadas
4109
+ strictMode: isProduction, // Auto-enabled in production, permissive in development
3913
4110
  validateOrigin: true // Valida que appName sea una app registrada
3914
4111
  };
3915
4112
 
4113
+ this._permissiveWarned = false;
4114
+
3916
4115
  this.stats = {
3917
4116
  emitted: 0,
3918
4117
  subscriptions: 0,
@@ -4010,6 +4209,19 @@ class WuEventBus {
4010
4209
  return `wu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
4011
4210
  }
4012
4211
 
4212
+ /**
4213
+ * WARN PERMISSIVE MODE: Log a one-time warning when strictMode is off
4214
+ * Alerts developers that events are flowing without authorization checks
4215
+ */
4216
+ _warnPermissiveMode() {
4217
+ if (this._permissiveWarned) return;
4218
+ this._permissiveWarned = true;
4219
+ logger.warn(
4220
+ '[WuEventBus] strictMode is disabled. Events are emitted without authorization checks. ' +
4221
+ 'Enable strictMode for production by calling enableStrictMode() or setting NODE_ENV=production.'
4222
+ );
4223
+ }
4224
+
4013
4225
  /**
4014
4226
  * 📢 EMIT: Emitir evento con validación de origen
4015
4227
  * @param {string} eventName - Nombre del evento
@@ -4019,6 +4231,11 @@ class WuEventBus {
4019
4231
  emit(eventName, data, options = {}) {
4020
4232
  const appName = options.appName || 'unknown';
4021
4233
 
4234
+ // Warn once if running in permissive mode (strictMode off)
4235
+ if (!this.config.strictMode) {
4236
+ this._warnPermissiveMode();
4237
+ }
4238
+
4022
4239
  // 🔐 Validar origen si está habilitado
4023
4240
  if (this.config.validateOrigin && this.config.strictMode) {
4024
4241
  if (!this._validateOrigin(eventName, appName, options.token)) {
@@ -4470,7 +4687,7 @@ class WuPerformance {
4470
4687
 
4471
4688
 
4472
4689
  class WuPluginSystem {
4473
- constructor(core) {
4690
+ constructor(core, options = {}) {
4474
4691
  this._core = core; // Privado - no expuesto a plugins
4475
4692
  this.plugins = new Map();
4476
4693
  this.hooks = new Map();
@@ -4494,7 +4711,7 @@ class WuPluginSystem {
4494
4711
  ];
4495
4712
 
4496
4713
  // 🔐 Timeout para hooks (evita que plugins bloqueen)
4497
- this.hookTimeout = 5000; // 5 segundos
4714
+ this.hookTimeout = options.hookTimeout || 5000; // Default: 5 segundos
4498
4715
 
4499
4716
  this.availableHooks.forEach(hook => {
4500
4717
  this.hooks.set(hook, []);
@@ -4565,8 +4782,61 @@ class WuPluginSystem {
4565
4782
  logger.warn('[WuPlugin] ⚠️ Plugin has unsafe access to core!');
4566
4783
  }
4567
4784
 
4568
- // Congelar API para evitar modificaciones
4569
- return Object.freeze(api);
4785
+ // Congelar API recursivamente para evitar modificaciones en cualquier nivel
4786
+ return WuPluginSystem._deepFreeze(api);
4787
+ }
4788
+
4789
+ /**
4790
+ * Deep-freeze an object and all its nested plain objects and arrays.
4791
+ * Functions, DOM nodes, and other non-plain types are left untouched
4792
+ * so they remain callable / functional. A WeakSet guards against
4793
+ * circular references.
4794
+ *
4795
+ * @param {*} obj - The value to deep-freeze
4796
+ * @param {WeakSet} [seen] - Internal tracker for circular references
4797
+ * @returns {*} The same object, now deeply frozen
4798
+ */
4799
+ static _deepFreeze(obj, seen) {
4800
+ if (obj === null || obj === undefined) {
4801
+ return obj;
4802
+ }
4803
+
4804
+ // Only freeze plain objects and arrays.
4805
+ // Functions must stay invocable; DOM nodes, class instances, etc.
4806
+ // should not be tampered with.
4807
+ const dominated = typeof obj === 'object';
4808
+ if (!dominated) {
4809
+ return obj;
4810
+ }
4811
+
4812
+ const isPlainObject =
4813
+ Object.getPrototypeOf(obj) === Object.prototype ||
4814
+ Object.getPrototypeOf(obj) === null;
4815
+ const isArray = Array.isArray(obj);
4816
+
4817
+ if (!isPlainObject && !isArray) {
4818
+ return obj;
4819
+ }
4820
+
4821
+ // Circular-reference guard
4822
+ if (!seen) {
4823
+ seen = new WeakSet();
4824
+ }
4825
+ if (seen.has(obj)) {
4826
+ return obj;
4827
+ }
4828
+ seen.add(obj);
4829
+
4830
+ // Recurse into own enumerable properties
4831
+ const keys = Object.keys(obj);
4832
+ for (let i = 0; i < keys.length; i++) {
4833
+ const value = obj[keys[i]];
4834
+ if (value !== null && value !== undefined && typeof value === 'object') {
4835
+ WuPluginSystem._deepFreeze(value, seen);
4836
+ }
4837
+ }
4838
+
4839
+ return Object.freeze(obj);
4570
4840
  }
4571
4841
 
4572
4842
  /**
@@ -5377,19 +5647,33 @@ class WuErrorBoundary {
5377
5647
  * @param {Object} context - Contexto
5378
5648
  */
5379
5649
  logError(error, context) {
5650
+ // Truncate stack to first 5 lines to prevent retaining large object references
5651
+ const stack = error.stack ? error.stack.split('\n').slice(0, 5).join('\n') : '';
5652
+
5653
+ // Shallow-copy context to avoid retaining references to live objects
5654
+ const safeContext = {};
5655
+ for (const key of Object.keys(context)) {
5656
+ const val = context[key];
5657
+ if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean' || val === null) {
5658
+ safeContext[key] = val;
5659
+ } else {
5660
+ safeContext[key] = String(val);
5661
+ }
5662
+ }
5663
+
5380
5664
  const errorEntry = {
5381
5665
  error: {
5382
5666
  name: error.name,
5383
5667
  message: error.message,
5384
- stack: error.stack
5668
+ stack
5385
5669
  },
5386
- context,
5670
+ context: safeContext,
5387
5671
  timestamp: Date.now()
5388
5672
  };
5389
5673
 
5390
5674
  this.errorLog.push(errorEntry);
5391
5675
 
5392
- // Mantener límite de log
5676
+ // Maintain log limit
5393
5677
  if (this.errorLog.length > this.maxErrorLog) {
5394
5678
  this.errorLog.shift();
5395
5679
  }
@@ -5857,19 +6141,28 @@ class WuHtmlParser {
5857
6141
  return this._cache.get(cacheKey);
5858
6142
  }
5859
6143
 
5860
- const temp = document.createElement('div');
5861
- temp.innerHTML = html;
6144
+ const parser = new DOMParser();
6145
+ const doc = parser.parseFromString(html, 'text/html');
5862
6146
 
5863
6147
  const inlineScripts = [];
5864
6148
  const externalScripts = [];
5865
6149
  const inlineStyles = [];
5866
6150
  const externalStyles = [];
5867
6151
 
5868
- this._extractResources(temp, {
6152
+ const ctx = {
5869
6153
  inlineScripts, externalScripts,
5870
6154
  inlineStyles, externalStyles,
5871
6155
  baseUrl
5872
- });
6156
+ };
6157
+
6158
+ // DOMParser moves <style>, <link>, and some <script> tags to <head>.
6159
+ // Extract resources from both head and body to capture everything.
6160
+ if (doc.head) {
6161
+ this._extractResources(doc.head, ctx);
6162
+ }
6163
+
6164
+ const temp = doc.body || doc.documentElement;
6165
+ this._extractResources(temp, ctx);
5873
6166
 
5874
6167
  const result = {
5875
6168
  dom: temp.innerHTML,
@@ -6000,6 +6293,52 @@ class WuHtmlParser {
6000
6293
 
6001
6294
  class WuScriptExecutor {
6002
6295
 
6296
+ /**
6297
+ * Dangerous patterns that indicate prototype pollution, sandbox escape,
6298
+ * or direct access to sensitive APIs. Each entry is a regex paired with
6299
+ * a human-readable label used in error messages.
6300
+ *
6301
+ * This is a tripwire, not a full parser. It catches the most common
6302
+ * attack vectors without the overhead of AST analysis.
6303
+ */
6304
+ static DANGEROUS_PATTERNS = [
6305
+ // Prototype pollution vectors
6306
+ { pattern: /constructor\s*\[\s*['"`]constructor['"`]\s*\]/, label: 'constructor chain access (sandbox escape)' },
6307
+ { pattern: /__proto__/, label: '__proto__ access (prototype pollution)' },
6308
+
6309
+ // Sandbox escape via proxy introspection
6310
+ { pattern: /Object\s*\.\s*getPrototypeOf\s*\(\s*proxy\s*\)/, label: 'Object.getPrototypeOf(proxy) (sandbox escape)' },
6311
+
6312
+ // Dynamic code generation that bypasses the sandbox
6313
+ { pattern: /Function\s*\(\s*['"`]/, label: 'Function() constructor (dynamic code generation)' },
6314
+ { pattern: /\beval\s*\(/, label: 'eval() (dynamic code execution)' },
6315
+
6316
+ // Dynamic import escapes the sandbox entirely (runs in global scope)
6317
+ { pattern: /\bimport\s*\(/, label: 'import() (dynamic import escapes sandbox)' },
6318
+
6319
+ // Direct cookie access (should go through proxy traps, not raw document)
6320
+ { pattern: /document\s*\.\s*cookie/, label: 'document.cookie (direct cookie access)' },
6321
+ ];
6322
+
6323
+ /**
6324
+ * Validate script text against known dangerous patterns before execution.
6325
+ * Throws if any pattern matches. This is intentionally lightweight --
6326
+ * pattern detection only, not a full parse.
6327
+ *
6328
+ * @param {string} scriptText - The raw script to validate
6329
+ * @param {string} appName - App identifier (for error context)
6330
+ * @throws {Error} If a dangerous pattern is detected
6331
+ */
6332
+ _validateScript(scriptText, appName) {
6333
+ for (const { pattern, label } of WuScriptExecutor.DANGEROUS_PATTERNS) {
6334
+ if (pattern.test(scriptText)) {
6335
+ const msg = `[ScriptExecutor] Blocked dangerous pattern in "${appName}": ${label}`;
6336
+ logger.wuError(msg);
6337
+ throw new Error(msg);
6338
+ }
6339
+ }
6340
+ }
6341
+
6003
6342
  /**
6004
6343
  * Execute a script string inside the proxy sandbox.
6005
6344
  *
@@ -6016,6 +6355,8 @@ class WuScriptExecutor {
6016
6355
 
6017
6356
  if (!scriptText || !scriptText.trim()) return;
6018
6357
 
6358
+ this._validateScript(scriptText, appName);
6359
+
6019
6360
  const sourceComment = sourceUrl ? `\n//# sourceURL=wu-sandbox:///${appName}/${sourceUrl}\n` : '';
6020
6361
 
6021
6362
  let wrappedCode;
@@ -7666,6 +8007,20 @@ class WuCore {
7666
8007
  } catch (error) {
7667
8008
  logger.wuError(`Mount attempt ${attempt + 1} failed for ${appName}:`, error);
7668
8009
 
8010
+ // Cleanup sandbox to prevent orphaned shadow DOMs
8011
+ try {
8012
+ if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
8013
+ const sb = this.sandbox.sandboxes.get(appName);
8014
+ if (sb && sb.proxySandbox) {
8015
+ sb.proxySandbox.deactivate();
8016
+ }
8017
+ this.sandbox.sandboxes.delete(appName);
8018
+ logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
8019
+ }
8020
+ } catch (cleanupError) {
8021
+ logger.wuWarn(`Sandbox cleanup failed for ${appName}:`, cleanupError);
8022
+ }
8023
+
7669
8024
  // Use error boundary for intelligent error handling
7670
8025
  const errorResult = await this.errorBoundary.handle(error, {
7671
8026
  appName,
@@ -15172,10 +15527,20 @@ if (typeof window !== 'undefined' && window.wu && window.wu._isWuFramework) {
15172
15527
  } else {
15173
15528
  wu = new WuCore();
15174
15529
  wu._isWuFramework = true;
15530
+
15531
+ // Also store on a Symbol key for collision-safe access
15532
+ if (typeof window !== 'undefined') {
15533
+ const WU_KEY = Symbol.for('wu-framework');
15534
+ window[WU_KEY] = wu;
15535
+ }
15175
15536
  }
15176
15537
 
15177
15538
  // Expose globally for microfrontends
15178
15539
  if (typeof window !== 'undefined') {
15540
+ // Warn if window.wu exists but is not a Wu Framework instance
15541
+ if (window.wu && !window.wu._isWuFramework) {
15542
+ console.warn('[Wu Framework] window.wu already exists and is not a Wu Framework instance. Overwriting. Use Symbol.for("wu-framework") for collision-safe access.');
15543
+ }
15179
15544
  window.wu = wu;
15180
15545
 
15181
15546
  if (!wu.version) {