wu-framework 1.1.14 → 1.1.16

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 (90) hide show
  1. package/LICENSE +39 -39
  2. package/README.md +408 -408
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15151 -15151
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js.map +1 -1
  7. package/dist/wu-framework.umd.js.map +1 -1
  8. package/integrations/astro/README.md +127 -127
  9. package/integrations/astro/WuApp.astro +63 -63
  10. package/integrations/astro/WuShell.astro +39 -39
  11. package/integrations/astro/index.js +68 -68
  12. package/integrations/astro/package.json +38 -38
  13. package/integrations/astro/types.d.ts +53 -53
  14. package/package.json +161 -161
  15. package/src/adapters/angular/ai.js +30 -30
  16. package/src/adapters/angular/index.d.ts +154 -154
  17. package/src/adapters/angular/index.js +932 -932
  18. package/src/adapters/angular.d.ts +3 -3
  19. package/src/adapters/angular.js +3 -3
  20. package/src/adapters/index.js +168 -168
  21. package/src/adapters/lit/ai.js +20 -20
  22. package/src/adapters/lit/index.d.ts +120 -120
  23. package/src/adapters/lit/index.js +721 -721
  24. package/src/adapters/lit.d.ts +3 -3
  25. package/src/adapters/lit.js +3 -3
  26. package/src/adapters/preact/ai.js +33 -33
  27. package/src/adapters/preact/index.d.ts +108 -108
  28. package/src/adapters/preact/index.js +661 -661
  29. package/src/adapters/preact.d.ts +3 -3
  30. package/src/adapters/preact.js +3 -3
  31. package/src/adapters/react/index.js +48 -54
  32. package/src/adapters/react.d.ts +3 -3
  33. package/src/adapters/react.js +3 -3
  34. package/src/adapters/shared.js +64 -64
  35. package/src/adapters/solid/ai.js +32 -32
  36. package/src/adapters/solid/index.d.ts +101 -101
  37. package/src/adapters/solid/index.js +586 -586
  38. package/src/adapters/solid.d.ts +3 -3
  39. package/src/adapters/solid.js +3 -3
  40. package/src/adapters/svelte/ai.js +31 -31
  41. package/src/adapters/svelte/index.d.ts +166 -166
  42. package/src/adapters/svelte/index.js +798 -798
  43. package/src/adapters/svelte.d.ts +3 -3
  44. package/src/adapters/svelte.js +3 -3
  45. package/src/adapters/vanilla/ai.js +30 -30
  46. package/src/adapters/vanilla/index.d.ts +179 -179
  47. package/src/adapters/vanilla/index.js +785 -785
  48. package/src/adapters/vanilla.d.ts +3 -3
  49. package/src/adapters/vanilla.js +3 -3
  50. package/src/adapters/vue/ai.js +52 -52
  51. package/src/adapters/vue/index.d.ts +299 -299
  52. package/src/adapters/vue/index.js +610 -610
  53. package/src/adapters/vue.d.ts +3 -3
  54. package/src/adapters/vue.js +3 -3
  55. package/src/ai/wu-ai-actions.js +261 -261
  56. package/src/ai/wu-ai-agent.js +546 -546
  57. package/src/ai/wu-ai-browser-primitives.js +354 -354
  58. package/src/ai/wu-ai-browser.js +380 -380
  59. package/src/ai/wu-ai-context.js +332 -332
  60. package/src/ai/wu-ai-conversation.js +613 -613
  61. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  62. package/src/ai/wu-ai-permissions.js +381 -381
  63. package/src/ai/wu-ai-provider.js +700 -700
  64. package/src/ai/wu-ai-schema.js +225 -225
  65. package/src/ai/wu-ai-triggers.js +396 -396
  66. package/src/ai/wu-ai.js +804 -804
  67. package/src/core/wu-app.js +236 -236
  68. package/src/core/wu-cache.js +477 -477
  69. package/src/core/wu-core.js +1398 -1398
  70. package/src/core/wu-error-boundary.js +382 -382
  71. package/src/core/wu-event-bus.js +348 -348
  72. package/src/core/wu-hooks.js +350 -350
  73. package/src/core/wu-html-parser.js +190 -190
  74. package/src/core/wu-iframe-sandbox.js +328 -328
  75. package/src/core/wu-loader.js +272 -272
  76. package/src/core/wu-logger.js +134 -134
  77. package/src/core/wu-manifest.js +509 -509
  78. package/src/core/wu-mcp-bridge.js +432 -432
  79. package/src/core/wu-overrides.js +510 -510
  80. package/src/core/wu-performance.js +228 -228
  81. package/src/core/wu-plugin.js +348 -348
  82. package/src/core/wu-prefetch.js +414 -414
  83. package/src/core/wu-proxy-sandbox.js +476 -476
  84. package/src/core/wu-sandbox.js +779 -779
  85. package/src/core/wu-script-executor.js +113 -113
  86. package/src/core/wu-snapshot-sandbox.js +227 -227
  87. package/src/core/wu-strategies.js +256 -256
  88. package/src/core/wu-style-bridge.js +477 -477
  89. package/src/index.js +224 -224
  90. 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
+ }