wu-framework 1.1.15 → 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.
Files changed (88) hide show
  1. package/README.md +52 -20
  2. package/dist/wu-framework.cjs.js +1 -1
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15511 -15146
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +1 -1
  7. package/dist/wu-framework.esm.js.map +1 -1
  8. package/dist/wu-framework.umd.js +1 -1
  9. package/dist/wu-framework.umd.js.map +1 -1
  10. package/package.json +166 -161
  11. package/src/adapters/angular/ai.js +30 -30
  12. package/src/adapters/angular/index.d.ts +154 -154
  13. package/src/adapters/angular/index.js +932 -932
  14. package/src/adapters/angular.d.ts +3 -3
  15. package/src/adapters/angular.js +3 -3
  16. package/src/adapters/index.js +168 -168
  17. package/src/adapters/lit/ai.js +20 -20
  18. package/src/adapters/lit/index.d.ts +120 -120
  19. package/src/adapters/lit/index.js +721 -721
  20. package/src/adapters/lit.d.ts +3 -3
  21. package/src/adapters/lit.js +3 -3
  22. package/src/adapters/preact/ai.js +33 -33
  23. package/src/adapters/preact/index.d.ts +108 -108
  24. package/src/adapters/preact/index.js +661 -661
  25. package/src/adapters/preact.d.ts +3 -3
  26. package/src/adapters/preact.js +3 -3
  27. package/src/adapters/react/index.js +48 -54
  28. package/src/adapters/react.d.ts +3 -3
  29. package/src/adapters/react.js +3 -3
  30. package/src/adapters/shared.js +64 -64
  31. package/src/adapters/solid/ai.js +32 -32
  32. package/src/adapters/solid/index.d.ts +101 -101
  33. package/src/adapters/solid/index.js +586 -586
  34. package/src/adapters/solid.d.ts +3 -3
  35. package/src/adapters/solid.js +3 -3
  36. package/src/adapters/svelte/ai.js +31 -31
  37. package/src/adapters/svelte/index.d.ts +166 -166
  38. package/src/adapters/svelte/index.js +798 -798
  39. package/src/adapters/svelte.d.ts +3 -3
  40. package/src/adapters/svelte.js +3 -3
  41. package/src/adapters/vanilla/ai.js +30 -30
  42. package/src/adapters/vanilla/index.d.ts +179 -179
  43. package/src/adapters/vanilla/index.js +785 -785
  44. package/src/adapters/vanilla.d.ts +3 -3
  45. package/src/adapters/vanilla.js +3 -3
  46. package/src/adapters/vue/ai.js +52 -52
  47. package/src/adapters/vue/index.d.ts +299 -299
  48. package/src/adapters/vue/index.js +610 -610
  49. package/src/adapters/vue.d.ts +3 -3
  50. package/src/adapters/vue.js +3 -3
  51. package/src/ai/wu-ai-actions.js +261 -261
  52. package/src/ai/wu-ai-agent.js +546 -546
  53. package/src/ai/wu-ai-browser-primitives.js +354 -354
  54. package/src/ai/wu-ai-browser.js +380 -380
  55. package/src/ai/wu-ai-context.js +332 -332
  56. package/src/ai/wu-ai-conversation.js +613 -613
  57. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  58. package/src/ai/wu-ai-permissions.js +381 -381
  59. package/src/ai/wu-ai-provider.js +700 -700
  60. package/src/ai/wu-ai-schema.js +225 -225
  61. package/src/ai/wu-ai-triggers.js +396 -396
  62. package/src/ai/wu-ai.js +804 -804
  63. package/src/core/wu-app.js +236 -236
  64. package/src/core/wu-cache.js +498 -477
  65. package/src/core/wu-core.js +1412 -1398
  66. package/src/core/wu-error-boundary.js +396 -382
  67. package/src/core/wu-event-bus.js +390 -348
  68. package/src/core/wu-hooks.js +350 -350
  69. package/src/core/wu-html-parser.js +199 -190
  70. package/src/core/wu-iframe-sandbox.js +328 -328
  71. package/src/core/wu-loader.js +385 -273
  72. package/src/core/wu-logger.js +142 -134
  73. package/src/core/wu-manifest.js +532 -509
  74. package/src/core/wu-mcp-bridge.js +432 -432
  75. package/src/core/wu-overrides.js +510 -510
  76. package/src/core/wu-performance.js +228 -228
  77. package/src/core/wu-plugin.js +401 -348
  78. package/src/core/wu-prefetch.js +414 -414
  79. package/src/core/wu-proxy-sandbox.js +477 -476
  80. package/src/core/wu-sandbox.js +779 -779
  81. package/src/core/wu-script-executor.js +161 -113
  82. package/src/core/wu-snapshot-sandbox.js +227 -227
  83. package/src/core/wu-store.js +13 -3
  84. package/src/core/wu-strategies.js +256 -256
  85. package/src/core/wu-style-bridge.js +477 -477
  86. package/src/index.d.ts +317 -0
  87. package/src/index.js +234 -224
  88. package/src/utils/dependency-resolver.js +327 -327
@@ -1,510 +1,510 @@
1
- /**
2
- * WU-OVERRIDES: Cookie-based URL overrides for QA/testing
3
- *
4
- * SECURITY MODEL:
5
- * - DISABLED in production by default (must opt-in with allowOverrides: true)
6
- * - Allowlist of trusted domains (only overrides to whitelisted hosts are accepted)
7
- * - Visual indicator when overrides are active (prevents silent phishing)
8
- *
9
- * How it works:
10
- * 1. QA sets a cookie: wu-override:cart=http://localhost:5173
11
- * 2. During wu.init(), overrides are parsed from document.cookie
12
- * 3. The URL for "cart" is replaced ONLY in that browser session
13
- * 4. Everyone else sees the production URL
14
- *
15
- * Cookie format:
16
- * wu-override:<appName>=<url>
17
- *
18
- * @example
19
- * // Enable in init (required in production)
20
- * wu.init({
21
- * apps: [...],
22
- * overrides: {
23
- * enabled: true,
24
- * allowedDomains: ['*.company.com', 'localhost', '*.vercel.app'],
25
- * showIndicator: true
26
- * }
27
- * });
28
- *
29
- * // Programmatic API
30
- * wu.override('cart', 'http://localhost:5173');
31
- * wu.removeOverride('cart');
32
- * wu.getOverrides();
33
- * wu.clearOverrides();
34
- */
35
-
36
- import { logger } from './wu-logger.js';
37
-
38
- const COOKIE_PREFIX = 'wu-override:';
39
-
40
- export class WuOverrides {
41
- constructor(config = {}) {
42
- // In-memory cache of active overrides (synced with cookies)
43
- this._overrides = new Map();
44
-
45
- // Security config
46
- this._allowedDomains = config.allowedDomains || [];
47
- this._showIndicator = config.showIndicator ?? true;
48
- this._indicatorElement = null;
49
-
50
- // Determine enabled state:
51
- // - If explicitly passed → use that value (respect user intent)
52
- // - If not passed → auto-detect from environment
53
- if (config.enabled !== undefined) {
54
- this._enabled = config.enabled;
55
- } else {
56
- this._enabled = this._isDevEnvironment();
57
- }
58
-
59
- // Parse existing cookies on construction (only if enabled)
60
- if (this._enabled) {
61
- this._parseFromCookies();
62
- }
63
- }
64
-
65
- // ─── Security: Environment Detection ─────────────────────────
66
-
67
- /**
68
- * Detect if we're in a development environment.
69
- * Overrides are auto-enabled in dev, disabled in production.
70
- */
71
- _isDevEnvironment() {
72
- if (typeof window === 'undefined') return false;
73
-
74
- const hostname = window.location?.hostname || '';
75
- const port = window.location?.port || '';
76
-
77
- // localhost, 127.0.0.1, or non-standard ports = development
78
- return (
79
- hostname === 'localhost' ||
80
- hostname === '127.0.0.1' ||
81
- hostname === '0.0.0.0' ||
82
- hostname.endsWith('.local') ||
83
- (port !== '' && port !== '80' && port !== '443')
84
- );
85
- }
86
-
87
- // ─── Security: Domain Allowlist ──────────────────────────────
88
-
89
- /**
90
- * Check if a URL's domain is in the allowlist.
91
- * If no allowlist is configured, all valid URLs are accepted (dev-mode behavior).
92
- * If an allowlist IS configured, only matching domains pass.
93
- *
94
- * @param {string} url
95
- * @returns {boolean}
96
- */
97
- _isDomainAllowed(url) {
98
- // No allowlist = allow everything (but only if overrides are enabled)
99
- if (this._allowedDomains.length === 0) return true;
100
-
101
- const hostname = this._extractHostname(url);
102
- if (!hostname) return false;
103
-
104
- for (const pattern of this._allowedDomains) {
105
- if (this._matchDomain(hostname, pattern)) return true;
106
- }
107
-
108
- return false;
109
- }
110
-
111
- /**
112
- * Extract hostname from a URL string.
113
- */
114
- _extractHostname(url) {
115
- try {
116
- // Handle localhost:PORT shorthand
117
- if (/^localhost(:\d+)?/.test(url)) return 'localhost';
118
-
119
- // Handle protocol-relative
120
- const normalized = url.startsWith('//') ? `https:${url}` : url;
121
- const parsed = new URL(normalized);
122
- return parsed.hostname;
123
- } catch {
124
- return null;
125
- }
126
- }
127
-
128
- /**
129
- * Match a hostname against a domain pattern.
130
- * Supports wildcard: *.company.com matches sub.company.com
131
- *
132
- * @param {string} hostname - e.g., 'cart.staging.company.com'
133
- * @param {string} pattern - e.g., '*.company.com' or 'localhost'
134
- * @returns {boolean}
135
- */
136
- _matchDomain(hostname, pattern) {
137
- // Exact match
138
- if (hostname === pattern) return true;
139
-
140
- // Wildcard match: *.company.com
141
- if (pattern.startsWith('*.')) {
142
- const suffix = pattern.substring(2); // 'company.com'
143
- return hostname === suffix || hostname.endsWith('.' + suffix);
144
- }
145
-
146
- return false;
147
- }
148
-
149
- // ─── Cookie Parsing ──────────────────────────────────────────
150
-
151
- /**
152
- * Parse all wu-override cookies from document.cookie.
153
- * Called automatically on construction and can be called manually to refresh.
154
- *
155
- * @returns {Map<string, string>} Map of appName → overrideUrl
156
- */
157
- _parseFromCookies() {
158
- this._overrides.clear();
159
-
160
- if (typeof document === 'undefined') return this._overrides;
161
-
162
- if (!this._enabled) {
163
- logger.wuDebug('[WuOverrides] Overrides disabled — skipping cookie parse');
164
- return this._overrides;
165
- }
166
-
167
- const cookies = document.cookie;
168
- if (!cookies) return this._overrides;
169
-
170
- // Split cookies and find wu-override:* entries
171
- const pairs = cookies.split(';');
172
-
173
- for (const pair of pairs) {
174
- const trimmed = pair.trim();
175
-
176
- if (!trimmed.startsWith(COOKIE_PREFIX)) continue;
177
-
178
- // wu-override:cart=http://localhost:5173
179
- const eqIndex = trimmed.indexOf('=');
180
- if (eqIndex === -1) continue;
181
-
182
- const appName = trimmed.substring(COOKIE_PREFIX.length, eqIndex).trim();
183
- const url = trimmed.substring(eqIndex + 1).trim();
184
-
185
- if (!appName || !url) continue;
186
-
187
- // Validate URL format
188
- if (!this._isValidUrl(url)) {
189
- logger.wuWarn(`[WuOverrides] Invalid override URL for "${appName}": ${url}`);
190
- continue;
191
- }
192
-
193
- // Validate domain is allowed
194
- if (!this._isDomainAllowed(url)) {
195
- logger.wuWarn(
196
- `[WuOverrides] BLOCKED: "${appName}" override to "${url}" — ` +
197
- `domain not in allowedDomains. ` +
198
- `Allowed: [${this._allowedDomains.join(', ')}]`
199
- );
200
- continue;
201
- }
202
-
203
- this._overrides.set(appName, url);
204
- logger.wuDebug(`[WuOverrides] Parsed override: ${appName} → ${url}`);
205
- }
206
-
207
- if (this._overrides.size > 0) {
208
- logger.wuInfo(
209
- `[WuOverrides] ${this._overrides.size} active override(s): ` +
210
- [...this._overrides.keys()].join(', ')
211
- );
212
- }
213
-
214
- return this._overrides;
215
- }
216
-
217
- // ─── Apply Overrides ─────────────────────────────────────────
218
-
219
- /**
220
- * Apply overrides to an array of app configs.
221
- * Mutates the url field of matching apps.
222
- * Called by WuCore during init, before registerApp.
223
- *
224
- * @param {Array<{name: string, url: string}>} apps - App configs to process
225
- * @returns {Array<{name: string, url: string, _originalUrl?: string}>} Same array, mutated
226
- */
227
- applyToApps(apps) {
228
- if (!this._enabled || this._overrides.size === 0) return apps;
229
-
230
- for (const app of apps) {
231
- const overrideUrl = this._overrides.get(app.name);
232
- if (overrideUrl) {
233
- app._originalUrl = app.url;
234
- app.url = overrideUrl;
235
- logger.wuInfo(
236
- `[WuOverrides] "${app.name}" overridden: ${app._originalUrl} → ${overrideUrl}`
237
- );
238
- }
239
- }
240
-
241
- // Show visual indicator if overrides were applied
242
- if (this._showIndicator && this._overrides.size > 0) {
243
- this._showOverrideIndicator();
244
- }
245
-
246
- return apps;
247
- }
248
-
249
- /**
250
- * Get the override URL for a specific app, or null if none.
251
- *
252
- * @param {string} appName
253
- * @returns {string|null}
254
- */
255
- getOverrideFor(appName) {
256
- return this._overrides.get(appName) || null;
257
- }
258
-
259
- // ─── Programmatic API ────────────────────────────────────────
260
-
261
- /**
262
- * Set an override for an app. Writes a cookie and updates in-memory cache.
263
- *
264
- * @param {string} appName - App to override
265
- * @param {string} url - Override URL (e.g., 'http://localhost:5173')
266
- * @param {Object} [options]
267
- * @param {number} [options.maxAge=86400] - Cookie max-age in seconds (default: 24h)
268
- * @param {string} [options.path='/'] - Cookie path
269
- */
270
- set(appName, url, options = {}) {
271
- if (!appName || !url) {
272
- throw new Error('[WuOverrides] appName and url are required');
273
- }
274
-
275
- if (!this._enabled) {
276
- throw new Error(
277
- '[WuOverrides] Overrides are disabled in this environment. ' +
278
- 'Enable with wu.init({ overrides: { enabled: true } })'
279
- );
280
- }
281
-
282
- if (!this._isValidUrl(url)) {
283
- throw new Error(`[WuOverrides] Invalid URL: ${url}`);
284
- }
285
-
286
- if (!this._isDomainAllowed(url)) {
287
- throw new Error(
288
- `[WuOverrides] Domain not allowed: "${this._extractHostname(url)}". ` +
289
- `Allowed: [${this._allowedDomains.join(', ')}]`
290
- );
291
- }
292
-
293
- const maxAge = options.maxAge ?? 86400; // 24 hours default
294
- const path = options.path ?? '/';
295
-
296
- // Set cookie
297
- if (typeof document !== 'undefined') {
298
- document.cookie =
299
- `${COOKIE_PREFIX}${appName}=${url}; path=${path}; max-age=${maxAge}; SameSite=Lax`;
300
- }
301
-
302
- // Update in-memory cache
303
- this._overrides.set(appName, url);
304
-
305
- // Update visual indicator
306
- if (this._showIndicator) {
307
- this._showOverrideIndicator();
308
- }
309
-
310
- logger.wuInfo(`[WuOverrides] Override set: ${appName} → ${url} (expires in ${maxAge}s)`);
311
- }
312
-
313
- /**
314
- * Remove an override for a specific app.
315
- *
316
- * @param {string} appName - App to remove override for
317
- */
318
- remove(appName) {
319
- // Delete cookie by setting max-age=0
320
- if (typeof document !== 'undefined') {
321
- document.cookie = `${COOKIE_PREFIX}${appName}=; path=/; max-age=0`;
322
- }
323
-
324
- this._overrides.delete(appName);
325
-
326
- // Update or remove indicator
327
- if (this._showIndicator) {
328
- if (this._overrides.size === 0) {
329
- this._removeOverrideIndicator();
330
- } else {
331
- this._showOverrideIndicator();
332
- }
333
- }
334
-
335
- logger.wuInfo(`[WuOverrides] Override removed: ${appName}`);
336
- }
337
-
338
- /**
339
- * Remove all overrides.
340
- */
341
- clearAll() {
342
- for (const appName of [...this._overrides.keys()]) {
343
- this.remove(appName);
344
- }
345
-
346
- this._removeOverrideIndicator();
347
-
348
- logger.wuInfo('[WuOverrides] All overrides cleared');
349
- }
350
-
351
- /**
352
- * Get all active overrides as a plain object.
353
- *
354
- * @returns {Object} { appName: url, ... }
355
- */
356
- getAll() {
357
- return Object.fromEntries(this._overrides);
358
- }
359
-
360
- /**
361
- * Check if any overrides are active.
362
- *
363
- * @returns {boolean}
364
- */
365
- hasOverrides() {
366
- return this._overrides.size > 0;
367
- }
368
-
369
- /**
370
- * Check if overrides are enabled in this environment.
371
- *
372
- * @returns {boolean}
373
- */
374
- isEnabled() {
375
- return this._enabled;
376
- }
377
-
378
- /**
379
- * Configure the override system.
380
- * Called by WuCore during init with config.overrides.
381
- *
382
- * @param {Object} config
383
- * @param {boolean} [config.enabled]
384
- * @param {string[]} [config.allowedDomains]
385
- * @param {boolean} [config.showIndicator]
386
- */
387
- configure(config = {}) {
388
- if (config.enabled !== undefined) {
389
- this._enabled = config.enabled;
390
- }
391
- if (config.allowedDomains) {
392
- this._allowedDomains = config.allowedDomains;
393
- }
394
- if (config.showIndicator !== undefined) {
395
- this._showIndicator = config.showIndicator;
396
- }
397
-
398
- // Re-parse cookies with new config
399
- if (this._enabled) {
400
- this._parseFromCookies();
401
- }
402
- }
403
-
404
- /**
405
- * Refresh overrides by re-parsing cookies.
406
- * Useful if cookies were modified externally (DevTools, other tabs).
407
- */
408
- refresh() {
409
- this._parseFromCookies();
410
- }
411
-
412
- // ─── Visual Indicator (anti-phishing) ────────────────────────
413
-
414
- /**
415
- * Show a fixed banner when overrides are active.
416
- * This prevents silent phishing — the user ALWAYS sees when
417
- * microfrontends are being loaded from non-standard URLs.
418
- */
419
- _showOverrideIndicator() {
420
- if (typeof document === 'undefined') return;
421
-
422
- // Remove existing indicator first
423
- this._removeOverrideIndicator();
424
-
425
- const indicator = document.createElement('div');
426
- indicator.id = 'wu-override-indicator';
427
-
428
- const overrideList = [...this._overrides.entries()]
429
- .map(([name, url]) => `${name} → ${url}`)
430
- .join(' | ');
431
-
432
- indicator.textContent = `WU OVERRIDE ACTIVE: ${overrideList}`;
433
-
434
- indicator.style.cssText = [
435
- 'position: fixed',
436
- 'bottom: 0',
437
- 'left: 0',
438
- 'right: 0',
439
- 'z-index: 2147483647',
440
- 'background: #f59e0b',
441
- 'color: #000',
442
- 'font-family: monospace',
443
- 'font-size: 12px',
444
- 'font-weight: bold',
445
- 'padding: 6px 12px',
446
- 'text-align: center',
447
- 'cursor: pointer',
448
- 'user-select: none',
449
- 'box-shadow: 0 -2px 8px rgba(0,0,0,0.2)'
450
- ].join(';');
451
-
452
- // Click to dismiss (but override stays active)
453
- indicator.addEventListener('click', () => {
454
- indicator.style.display = 'none';
455
- });
456
-
457
- // Double-click to clear all overrides
458
- indicator.addEventListener('dblclick', (e) => {
459
- e.preventDefault();
460
- this.clearAll();
461
- });
462
-
463
- indicator.title = 'Click to hide | Double-click to clear all overrides';
464
-
465
- document.body.appendChild(indicator);
466
- this._indicatorElement = indicator;
467
- }
468
-
469
- /**
470
- * Remove the visual indicator.
471
- */
472
- _removeOverrideIndicator() {
473
- if (this._indicatorElement) {
474
- this._indicatorElement.remove();
475
- this._indicatorElement = null;
476
- }
477
- // Also remove by ID in case element reference was lost
478
- if (typeof document !== 'undefined') {
479
- const existing = document.getElementById('wu-override-indicator');
480
- if (existing) existing.remove();
481
- }
482
- }
483
-
484
- // ─── Validation ──────────────────────────────────────────────
485
-
486
- _isValidUrl(url) {
487
- // Accept http://, https://, and // protocol-relative
488
- if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
489
- return true;
490
- }
491
- // Accept localhost shorthand like localhost:3000
492
- if (/^localhost(:\d+)?/.test(url)) {
493
- return true;
494
- }
495
- return false;
496
- }
497
-
498
- // ─── Stats ───────────────────────────────────────────────────
499
-
500
- getStats() {
501
- return {
502
- enabled: this._enabled,
503
- activeOverrides: this._overrides.size,
504
- overrides: this.getAll(),
505
- allowedDomains: this._allowedDomains,
506
- showIndicator: this._showIndicator,
507
- environment: this._isDevEnvironment() ? 'development' : 'production'
508
- };
509
- }
510
- }
1
+ /**
2
+ * WU-OVERRIDES: Cookie-based URL overrides for QA/testing
3
+ *
4
+ * SECURITY MODEL:
5
+ * - DISABLED in production by default (must opt-in with allowOverrides: true)
6
+ * - Allowlist of trusted domains (only overrides to whitelisted hosts are accepted)
7
+ * - Visual indicator when overrides are active (prevents silent phishing)
8
+ *
9
+ * How it works:
10
+ * 1. QA sets a cookie: wu-override:cart=http://localhost:5173
11
+ * 2. During wu.init(), overrides are parsed from document.cookie
12
+ * 3. The URL for "cart" is replaced ONLY in that browser session
13
+ * 4. Everyone else sees the production URL
14
+ *
15
+ * Cookie format:
16
+ * wu-override:<appName>=<url>
17
+ *
18
+ * @example
19
+ * // Enable in init (required in production)
20
+ * wu.init({
21
+ * apps: [...],
22
+ * overrides: {
23
+ * enabled: true,
24
+ * allowedDomains: ['*.company.com', 'localhost', '*.vercel.app'],
25
+ * showIndicator: true
26
+ * }
27
+ * });
28
+ *
29
+ * // Programmatic API
30
+ * wu.override('cart', 'http://localhost:5173');
31
+ * wu.removeOverride('cart');
32
+ * wu.getOverrides();
33
+ * wu.clearOverrides();
34
+ */
35
+
36
+ import { logger } from './wu-logger.js';
37
+
38
+ const COOKIE_PREFIX = 'wu-override:';
39
+
40
+ export class WuOverrides {
41
+ constructor(config = {}) {
42
+ // In-memory cache of active overrides (synced with cookies)
43
+ this._overrides = new Map();
44
+
45
+ // Security config
46
+ this._allowedDomains = config.allowedDomains || [];
47
+ this._showIndicator = config.showIndicator ?? true;
48
+ this._indicatorElement = null;
49
+
50
+ // Determine enabled state:
51
+ // - If explicitly passed → use that value (respect user intent)
52
+ // - If not passed → auto-detect from environment
53
+ if (config.enabled !== undefined) {
54
+ this._enabled = config.enabled;
55
+ } else {
56
+ this._enabled = this._isDevEnvironment();
57
+ }
58
+
59
+ // Parse existing cookies on construction (only if enabled)
60
+ if (this._enabled) {
61
+ this._parseFromCookies();
62
+ }
63
+ }
64
+
65
+ // ─── Security: Environment Detection ─────────────────────────
66
+
67
+ /**
68
+ * Detect if we're in a development environment.
69
+ * Overrides are auto-enabled in dev, disabled in production.
70
+ */
71
+ _isDevEnvironment() {
72
+ if (typeof window === 'undefined') return false;
73
+
74
+ const hostname = window.location?.hostname || '';
75
+ const port = window.location?.port || '';
76
+
77
+ // localhost, 127.0.0.1, or non-standard ports = development
78
+ return (
79
+ hostname === 'localhost' ||
80
+ hostname === '127.0.0.1' ||
81
+ hostname === '0.0.0.0' ||
82
+ hostname.endsWith('.local') ||
83
+ (port !== '' && port !== '80' && port !== '443')
84
+ );
85
+ }
86
+
87
+ // ─── Security: Domain Allowlist ──────────────────────────────
88
+
89
+ /**
90
+ * Check if a URL's domain is in the allowlist.
91
+ * If no allowlist is configured, all valid URLs are accepted (dev-mode behavior).
92
+ * If an allowlist IS configured, only matching domains pass.
93
+ *
94
+ * @param {string} url
95
+ * @returns {boolean}
96
+ */
97
+ _isDomainAllowed(url) {
98
+ // No allowlist = allow everything (but only if overrides are enabled)
99
+ if (this._allowedDomains.length === 0) return true;
100
+
101
+ const hostname = this._extractHostname(url);
102
+ if (!hostname) return false;
103
+
104
+ for (const pattern of this._allowedDomains) {
105
+ if (this._matchDomain(hostname, pattern)) return true;
106
+ }
107
+
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Extract hostname from a URL string.
113
+ */
114
+ _extractHostname(url) {
115
+ try {
116
+ // Handle localhost:PORT shorthand
117
+ if (/^localhost(:\d+)?/.test(url)) return 'localhost';
118
+
119
+ // Handle protocol-relative
120
+ const normalized = url.startsWith('//') ? `https:${url}` : url;
121
+ const parsed = new URL(normalized);
122
+ return parsed.hostname;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Match a hostname against a domain pattern.
130
+ * Supports wildcard: *.company.com matches sub.company.com
131
+ *
132
+ * @param {string} hostname - e.g., 'cart.staging.company.com'
133
+ * @param {string} pattern - e.g., '*.company.com' or 'localhost'
134
+ * @returns {boolean}
135
+ */
136
+ _matchDomain(hostname, pattern) {
137
+ // Exact match
138
+ if (hostname === pattern) return true;
139
+
140
+ // Wildcard match: *.company.com
141
+ if (pattern.startsWith('*.')) {
142
+ const suffix = pattern.substring(2); // 'company.com'
143
+ return hostname === suffix || hostname.endsWith('.' + suffix);
144
+ }
145
+
146
+ return false;
147
+ }
148
+
149
+ // ─── Cookie Parsing ──────────────────────────────────────────
150
+
151
+ /**
152
+ * Parse all wu-override cookies from document.cookie.
153
+ * Called automatically on construction and can be called manually to refresh.
154
+ *
155
+ * @returns {Map<string, string>} Map of appName → overrideUrl
156
+ */
157
+ _parseFromCookies() {
158
+ this._overrides.clear();
159
+
160
+ if (typeof document === 'undefined') return this._overrides;
161
+
162
+ if (!this._enabled) {
163
+ logger.wuDebug('[WuOverrides] Overrides disabled — skipping cookie parse');
164
+ return this._overrides;
165
+ }
166
+
167
+ const cookies = document.cookie;
168
+ if (!cookies) return this._overrides;
169
+
170
+ // Split cookies and find wu-override:* entries
171
+ const pairs = cookies.split(';');
172
+
173
+ for (const pair of pairs) {
174
+ const trimmed = pair.trim();
175
+
176
+ if (!trimmed.startsWith(COOKIE_PREFIX)) continue;
177
+
178
+ // wu-override:cart=http://localhost:5173
179
+ const eqIndex = trimmed.indexOf('=');
180
+ if (eqIndex === -1) continue;
181
+
182
+ const appName = trimmed.substring(COOKIE_PREFIX.length, eqIndex).trim();
183
+ const url = trimmed.substring(eqIndex + 1).trim();
184
+
185
+ if (!appName || !url) continue;
186
+
187
+ // Validate URL format
188
+ if (!this._isValidUrl(url)) {
189
+ logger.wuWarn(`[WuOverrides] Invalid override URL for "${appName}": ${url}`);
190
+ continue;
191
+ }
192
+
193
+ // Validate domain is allowed
194
+ if (!this._isDomainAllowed(url)) {
195
+ logger.wuWarn(
196
+ `[WuOverrides] BLOCKED: "${appName}" override to "${url}" — ` +
197
+ `domain not in allowedDomains. ` +
198
+ `Allowed: [${this._allowedDomains.join(', ')}]`
199
+ );
200
+ continue;
201
+ }
202
+
203
+ this._overrides.set(appName, url);
204
+ logger.wuDebug(`[WuOverrides] Parsed override: ${appName} → ${url}`);
205
+ }
206
+
207
+ if (this._overrides.size > 0) {
208
+ logger.wuInfo(
209
+ `[WuOverrides] ${this._overrides.size} active override(s): ` +
210
+ [...this._overrides.keys()].join(', ')
211
+ );
212
+ }
213
+
214
+ return this._overrides;
215
+ }
216
+
217
+ // ─── Apply Overrides ─────────────────────────────────────────
218
+
219
+ /**
220
+ * Apply overrides to an array of app configs.
221
+ * Mutates the url field of matching apps.
222
+ * Called by WuCore during init, before registerApp.
223
+ *
224
+ * @param {Array<{name: string, url: string}>} apps - App configs to process
225
+ * @returns {Array<{name: string, url: string, _originalUrl?: string}>} Same array, mutated
226
+ */
227
+ applyToApps(apps) {
228
+ if (!this._enabled || this._overrides.size === 0) return apps;
229
+
230
+ for (const app of apps) {
231
+ const overrideUrl = this._overrides.get(app.name);
232
+ if (overrideUrl) {
233
+ app._originalUrl = app.url;
234
+ app.url = overrideUrl;
235
+ logger.wuInfo(
236
+ `[WuOverrides] "${app.name}" overridden: ${app._originalUrl} → ${overrideUrl}`
237
+ );
238
+ }
239
+ }
240
+
241
+ // Show visual indicator if overrides were applied
242
+ if (this._showIndicator && this._overrides.size > 0) {
243
+ this._showOverrideIndicator();
244
+ }
245
+
246
+ return apps;
247
+ }
248
+
249
+ /**
250
+ * Get the override URL for a specific app, or null if none.
251
+ *
252
+ * @param {string} appName
253
+ * @returns {string|null}
254
+ */
255
+ getOverrideFor(appName) {
256
+ return this._overrides.get(appName) || null;
257
+ }
258
+
259
+ // ─── Programmatic API ────────────────────────────────────────
260
+
261
+ /**
262
+ * Set an override for an app. Writes a cookie and updates in-memory cache.
263
+ *
264
+ * @param {string} appName - App to override
265
+ * @param {string} url - Override URL (e.g., 'http://localhost:5173')
266
+ * @param {Object} [options]
267
+ * @param {number} [options.maxAge=86400] - Cookie max-age in seconds (default: 24h)
268
+ * @param {string} [options.path='/'] - Cookie path
269
+ */
270
+ set(appName, url, options = {}) {
271
+ if (!appName || !url) {
272
+ throw new Error('[WuOverrides] appName and url are required');
273
+ }
274
+
275
+ if (!this._enabled) {
276
+ throw new Error(
277
+ '[WuOverrides] Overrides are disabled in this environment. ' +
278
+ 'Enable with wu.init({ overrides: { enabled: true } })'
279
+ );
280
+ }
281
+
282
+ if (!this._isValidUrl(url)) {
283
+ throw new Error(`[WuOverrides] Invalid URL: ${url}`);
284
+ }
285
+
286
+ if (!this._isDomainAllowed(url)) {
287
+ throw new Error(
288
+ `[WuOverrides] Domain not allowed: "${this._extractHostname(url)}". ` +
289
+ `Allowed: [${this._allowedDomains.join(', ')}]`
290
+ );
291
+ }
292
+
293
+ const maxAge = options.maxAge ?? 86400; // 24 hours default
294
+ const path = options.path ?? '/';
295
+
296
+ // Set cookie
297
+ if (typeof document !== 'undefined') {
298
+ document.cookie =
299
+ `${COOKIE_PREFIX}${appName}=${url}; path=${path}; max-age=${maxAge}; SameSite=Lax`;
300
+ }
301
+
302
+ // Update in-memory cache
303
+ this._overrides.set(appName, url);
304
+
305
+ // Update visual indicator
306
+ if (this._showIndicator) {
307
+ this._showOverrideIndicator();
308
+ }
309
+
310
+ logger.wuInfo(`[WuOverrides] Override set: ${appName} → ${url} (expires in ${maxAge}s)`);
311
+ }
312
+
313
+ /**
314
+ * Remove an override for a specific app.
315
+ *
316
+ * @param {string} appName - App to remove override for
317
+ */
318
+ remove(appName) {
319
+ // Delete cookie by setting max-age=0
320
+ if (typeof document !== 'undefined') {
321
+ document.cookie = `${COOKIE_PREFIX}${appName}=; path=/; max-age=0`;
322
+ }
323
+
324
+ this._overrides.delete(appName);
325
+
326
+ // Update or remove indicator
327
+ if (this._showIndicator) {
328
+ if (this._overrides.size === 0) {
329
+ this._removeOverrideIndicator();
330
+ } else {
331
+ this._showOverrideIndicator();
332
+ }
333
+ }
334
+
335
+ logger.wuInfo(`[WuOverrides] Override removed: ${appName}`);
336
+ }
337
+
338
+ /**
339
+ * Remove all overrides.
340
+ */
341
+ clearAll() {
342
+ for (const appName of [...this._overrides.keys()]) {
343
+ this.remove(appName);
344
+ }
345
+
346
+ this._removeOverrideIndicator();
347
+
348
+ logger.wuInfo('[WuOverrides] All overrides cleared');
349
+ }
350
+
351
+ /**
352
+ * Get all active overrides as a plain object.
353
+ *
354
+ * @returns {Object} { appName: url, ... }
355
+ */
356
+ getAll() {
357
+ return Object.fromEntries(this._overrides);
358
+ }
359
+
360
+ /**
361
+ * Check if any overrides are active.
362
+ *
363
+ * @returns {boolean}
364
+ */
365
+ hasOverrides() {
366
+ return this._overrides.size > 0;
367
+ }
368
+
369
+ /**
370
+ * Check if overrides are enabled in this environment.
371
+ *
372
+ * @returns {boolean}
373
+ */
374
+ isEnabled() {
375
+ return this._enabled;
376
+ }
377
+
378
+ /**
379
+ * Configure the override system.
380
+ * Called by WuCore during init with config.overrides.
381
+ *
382
+ * @param {Object} config
383
+ * @param {boolean} [config.enabled]
384
+ * @param {string[]} [config.allowedDomains]
385
+ * @param {boolean} [config.showIndicator]
386
+ */
387
+ configure(config = {}) {
388
+ if (config.enabled !== undefined) {
389
+ this._enabled = config.enabled;
390
+ }
391
+ if (config.allowedDomains) {
392
+ this._allowedDomains = config.allowedDomains;
393
+ }
394
+ if (config.showIndicator !== undefined) {
395
+ this._showIndicator = config.showIndicator;
396
+ }
397
+
398
+ // Re-parse cookies with new config
399
+ if (this._enabled) {
400
+ this._parseFromCookies();
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Refresh overrides by re-parsing cookies.
406
+ * Useful if cookies were modified externally (DevTools, other tabs).
407
+ */
408
+ refresh() {
409
+ this._parseFromCookies();
410
+ }
411
+
412
+ // ─── Visual Indicator (anti-phishing) ────────────────────────
413
+
414
+ /**
415
+ * Show a fixed banner when overrides are active.
416
+ * This prevents silent phishing — the user ALWAYS sees when
417
+ * microfrontends are being loaded from non-standard URLs.
418
+ */
419
+ _showOverrideIndicator() {
420
+ if (typeof document === 'undefined') return;
421
+
422
+ // Remove existing indicator first
423
+ this._removeOverrideIndicator();
424
+
425
+ const indicator = document.createElement('div');
426
+ indicator.id = 'wu-override-indicator';
427
+
428
+ const overrideList = [...this._overrides.entries()]
429
+ .map(([name, url]) => `${name} → ${url}`)
430
+ .join(' | ');
431
+
432
+ indicator.textContent = `WU OVERRIDE ACTIVE: ${overrideList}`;
433
+
434
+ indicator.style.cssText = [
435
+ 'position: fixed',
436
+ 'bottom: 0',
437
+ 'left: 0',
438
+ 'right: 0',
439
+ 'z-index: 2147483647',
440
+ 'background: #f59e0b',
441
+ 'color: #000',
442
+ 'font-family: monospace',
443
+ 'font-size: 12px',
444
+ 'font-weight: bold',
445
+ 'padding: 6px 12px',
446
+ 'text-align: center',
447
+ 'cursor: pointer',
448
+ 'user-select: none',
449
+ 'box-shadow: 0 -2px 8px rgba(0,0,0,0.2)'
450
+ ].join(';');
451
+
452
+ // Click to dismiss (but override stays active)
453
+ indicator.addEventListener('click', () => {
454
+ indicator.style.display = 'none';
455
+ });
456
+
457
+ // Double-click to clear all overrides
458
+ indicator.addEventListener('dblclick', (e) => {
459
+ e.preventDefault();
460
+ this.clearAll();
461
+ });
462
+
463
+ indicator.title = 'Click to hide | Double-click to clear all overrides';
464
+
465
+ document.body.appendChild(indicator);
466
+ this._indicatorElement = indicator;
467
+ }
468
+
469
+ /**
470
+ * Remove the visual indicator.
471
+ */
472
+ _removeOverrideIndicator() {
473
+ if (this._indicatorElement) {
474
+ this._indicatorElement.remove();
475
+ this._indicatorElement = null;
476
+ }
477
+ // Also remove by ID in case element reference was lost
478
+ if (typeof document !== 'undefined') {
479
+ const existing = document.getElementById('wu-override-indicator');
480
+ if (existing) existing.remove();
481
+ }
482
+ }
483
+
484
+ // ─── Validation ──────────────────────────────────────────────
485
+
486
+ _isValidUrl(url) {
487
+ // Accept http://, https://, and // protocol-relative
488
+ if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//')) {
489
+ return true;
490
+ }
491
+ // Accept localhost shorthand like localhost:3000
492
+ if (/^localhost(:\d+)?/.test(url)) {
493
+ return true;
494
+ }
495
+ return false;
496
+ }
497
+
498
+ // ─── Stats ───────────────────────────────────────────────────
499
+
500
+ getStats() {
501
+ return {
502
+ enabled: this._enabled,
503
+ activeOverrides: this._overrides.size,
504
+ overrides: this.getAll(),
505
+ allowedDomains: this._allowedDomains,
506
+ showIndicator: this._showIndicator,
507
+ environment: this._isDevEnvironment() ? 'development' : 'production'
508
+ };
509
+ }
510
+ }