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,476 +1,476 @@
1
- /**
2
- * WU-PROXY-SANDBOX: Hardened JavaScript Isolation
3
- *
4
- * ES6 Proxy-based sandbox with side-effect tracking:
5
- * - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
6
- * - Event listener tracking (window + document addEventListener)
7
- * - DOM scoping (querySelector/querySelectorAll → shadow root)
8
- * - Storage scoping (localStorage/sessionStorage → prefixed keys)
9
- *
10
- * All tracked side effects are automatically cleaned up on deactivate().
11
- */
12
-
13
- import { logger } from './wu-logger.js';
14
-
15
- export class WuProxySandbox {
16
- constructor(appName) {
17
- this.appName = appName;
18
- this.proxy = null;
19
- this.fakeWindow = Object.create(null);
20
- this.active = false;
21
- this.modifiedKeys = new Set();
22
-
23
- // --- Side-effect tracking ---
24
- this._timers = new Set();
25
- this._intervals = new Set();
26
- this._rafs = new Set();
27
- this._eventListeners = []; // [{target, event, handler, options}]
28
-
29
- // --- DOM & Storage scoping ---
30
- this._container = null;
31
- this._shadowRoot = null;
32
- this._scopedDocument = null;
33
- this._scopedLocalStorage = null;
34
- this._scopedSessionStorage = null;
35
-
36
- // --- Window patching state ---
37
- this._patched = false;
38
- this._originals = null;
39
- }
40
-
41
- /**
42
- * Set the DOM scope for this sandbox.
43
- * Must be called before activate() for DOM scoping to work.
44
- * @param {HTMLElement} container - App container element
45
- * @param {ShadowRoot} shadowRoot - Shadow root containing the container
46
- */
47
- setContainer(container, shadowRoot) {
48
- this._container = container;
49
- this._shadowRoot = shadowRoot;
50
- }
51
-
52
- /**
53
- * Activate the sandbox. Creates the Proxy and starts tracking.
54
- * @returns {Proxy} The sandboxed window proxy
55
- */
56
- activate() {
57
- if (this.active) return this.proxy;
58
-
59
- const self = this;
60
-
61
- this.proxy = new Proxy(window, {
62
- get(target, prop) {
63
- // 1. App's own isolated globals
64
- if (prop in self.fakeWindow) {
65
- return self.fakeWindow[prop];
66
- }
67
-
68
- // 2. Intercepted APIs
69
- const intercepted = self._intercept(prop, target);
70
- if (intercepted !== undefined) {
71
- return intercepted;
72
- }
73
-
74
- // 3. Real window value with correct binding
75
- const value = target[prop];
76
- if (typeof value === 'function' && !self._isConstructor(value)) {
77
- return value.bind(target);
78
- }
79
- return value;
80
- },
81
-
82
- set(target, prop, value) {
83
- self.fakeWindow[prop] = value;
84
- self.modifiedKeys.add(prop);
85
- return true;
86
- },
87
-
88
- has(target, prop) {
89
- return prop in self.fakeWindow || prop in target;
90
- },
91
-
92
- deleteProperty(target, prop) {
93
- if (prop in self.fakeWindow) {
94
- delete self.fakeWindow[prop];
95
- self.modifiedKeys.delete(prop);
96
- return true;
97
- }
98
- return false;
99
- }
100
- });
101
-
102
- this.active = true;
103
- logger.wuDebug(`[ProxySandbox] Activated for ${this.appName}`);
104
- return this.proxy;
105
- }
106
-
107
- /**
108
- * Deactivate the sandbox. Cleans up ALL tracked side effects.
109
- */
110
- deactivate() {
111
- if (!this.active) return;
112
-
113
- // Unpatch window if patched
114
- this.unpatchWindow();
115
-
116
- // --- Clean timers ---
117
- for (const id of this._timers) {
118
- try { clearTimeout(id); } catch {}
119
- }
120
- for (const id of this._intervals) {
121
- try { clearInterval(id); } catch {}
122
- }
123
- for (const id of this._rafs) {
124
- try { cancelAnimationFrame(id); } catch {}
125
- }
126
-
127
- const timerCount = this._timers.size + this._intervals.size + this._rafs.size;
128
- this._timers.clear();
129
- this._intervals.clear();
130
- this._rafs.clear();
131
-
132
- // --- Clean event listeners ---
133
- const listenerCount = this._eventListeners.length;
134
- for (const { target, event, handler, options } of this._eventListeners) {
135
- try { target.removeEventListener(event, handler, options); } catch {}
136
- }
137
- this._eventListeners = [];
138
-
139
- // --- Clean namespace ---
140
- this.fakeWindow = Object.create(null);
141
- this.modifiedKeys.clear();
142
- this._scopedDocument = null;
143
- this._scopedLocalStorage = null;
144
- this._scopedSessionStorage = null;
145
- this.proxy = null;
146
- this.active = false;
147
-
148
- if (timerCount > 0 || listenerCount > 0) {
149
- logger.wuDebug(
150
- `[ProxySandbox] ${this.appName} cleanup: ${timerCount} timers, ${listenerCount} listeners`
151
- );
152
- }
153
- logger.wuDebug(`[ProxySandbox] Deactivated for ${this.appName}`);
154
- }
155
-
156
- // ================================================================
157
- // WINDOW PATCHING - patches real window APIs during module loading
158
- // ================================================================
159
-
160
- /**
161
- * Patch real window APIs to track side effects from global code.
162
- * Call before loading app module, unpatch after.
163
- *
164
- * IMPORTANT: Uses closure over originals so patched functions remain
165
- * valid even after unpatchWindow() — prevents crashes when frameworks
166
- * (React 19, etc.) cache references to patched setTimeout during import.
167
- */
168
- patchWindow() {
169
- if (this._patched) return;
170
-
171
- const self = this;
172
-
173
- // Capture originals in a local closure that survives unpatch
174
- const originals = {
175
- setTimeout: window.setTimeout,
176
- clearTimeout: window.clearTimeout,
177
- setInterval: window.setInterval,
178
- clearInterval: window.clearInterval,
179
- requestAnimationFrame: window.requestAnimationFrame,
180
- cancelAnimationFrame: window.cancelAnimationFrame,
181
- addEventListener: window.addEventListener,
182
- removeEventListener: window.removeEventListener
183
- };
184
-
185
- // Store reference (used by unpatchWindow to restore)
186
- this._originals = originals;
187
-
188
- // Patch timers — closure captures `originals`, not `self._originals`
189
- window.setTimeout = function(fn, delay, ...args) {
190
- const id = originals.setTimeout.call(window, fn, delay, ...args);
191
- if (self._patched) self._timers.add(id);
192
- return id;
193
- };
194
- window.clearTimeout = function(id) {
195
- self._timers.delete(id);
196
- return originals.clearTimeout.call(window, id);
197
- };
198
- window.setInterval = function(fn, delay, ...args) {
199
- const id = originals.setInterval.call(window, fn, delay, ...args);
200
- if (self._patched) self._intervals.add(id);
201
- return id;
202
- };
203
- window.clearInterval = function(id) {
204
- self._intervals.delete(id);
205
- return originals.clearInterval.call(window, id);
206
- };
207
- window.requestAnimationFrame = function(fn) {
208
- const id = originals.requestAnimationFrame.call(window, fn);
209
- if (self._patched) self._rafs.add(id);
210
- return id;
211
- };
212
- window.cancelAnimationFrame = function(id) {
213
- self._rafs.delete(id);
214
- return originals.cancelAnimationFrame.call(window, id);
215
- };
216
-
217
- // Patch event listeners
218
- window.addEventListener = function(event, handler, options) {
219
- if (self._patched) self._eventListeners.push({ target: window, event, handler, options });
220
- return originals.addEventListener.call(window, event, handler, options);
221
- };
222
- window.removeEventListener = function(event, handler, options) {
223
- self._eventListeners = self._eventListeners.filter(
224
- l => !(l.target === window && l.event === event && l.handler === handler)
225
- );
226
- return originals.removeEventListener.call(window, event, handler, options);
227
- };
228
-
229
- this._patched = true;
230
- logger.wuDebug(`[ProxySandbox] Window patched for ${this.appName}`);
231
- }
232
-
233
- /**
234
- * Restore original window APIs.
235
- * Safe: patched functions still work via closure even after restore.
236
- */
237
- unpatchWindow() {
238
- if (!this._patched || !this._originals) return;
239
-
240
- window.setTimeout = this._originals.setTimeout;
241
- window.clearTimeout = this._originals.clearTimeout;
242
- window.setInterval = this._originals.setInterval;
243
- window.clearInterval = this._originals.clearInterval;
244
- window.requestAnimationFrame = this._originals.requestAnimationFrame;
245
- window.cancelAnimationFrame = this._originals.cancelAnimationFrame;
246
- window.addEventListener = this._originals.addEventListener;
247
- window.removeEventListener = this._originals.removeEventListener;
248
-
249
- // NOTE: Do NOT null _originals — patched closures may still reference
250
- // the sandbox instance (e.g. React scheduler caches setTimeout).
251
- // The closure uses `originals` (local const), not `this._originals`.
252
- this._patched = false;
253
- logger.wuDebug(`[ProxySandbox] Window unpatched for ${this.appName}`);
254
- }
255
-
256
- // ================================================================
257
- // PROXY INTERCEPTS - for code running through the proxy
258
- // ================================================================
259
-
260
- /**
261
- * Intercept property access on the proxy.
262
- * Returns wrapped API or undefined to fall through.
263
- */
264
- _intercept(prop, target) {
265
- const self = this;
266
-
267
- switch (prop) {
268
- // --- Timer hijacking ---
269
- case 'setTimeout':
270
- return function(fn, delay, ...args) {
271
- const id = target.setTimeout(fn, delay, ...args);
272
- self._timers.add(id);
273
- return id;
274
- };
275
- case 'clearTimeout':
276
- return function(id) {
277
- self._timers.delete(id);
278
- target.clearTimeout(id);
279
- };
280
- case 'setInterval':
281
- return function(fn, delay, ...args) {
282
- const id = target.setInterval(fn, delay, ...args);
283
- self._intervals.add(id);
284
- return id;
285
- };
286
- case 'clearInterval':
287
- return function(id) {
288
- self._intervals.delete(id);
289
- target.clearInterval(id);
290
- };
291
- case 'requestAnimationFrame':
292
- return function(fn) {
293
- const id = target.requestAnimationFrame(fn);
294
- self._rafs.add(id);
295
- return id;
296
- };
297
- case 'cancelAnimationFrame':
298
- return function(id) {
299
- self._rafs.delete(id);
300
- target.cancelAnimationFrame(id);
301
- };
302
-
303
- // --- Event listener tracking ---
304
- case 'addEventListener':
305
- return function(event, handler, options) {
306
- self._eventListeners.push({ target, event, handler, options });
307
- target.addEventListener(event, handler, options);
308
- };
309
- case 'removeEventListener':
310
- return function(event, handler, options) {
311
- self._eventListeners = self._eventListeners.filter(
312
- l => !(l.target === target && l.event === event && l.handler === handler)
313
- );
314
- target.removeEventListener(event, handler, options);
315
- };
316
-
317
- // --- DOM scoping ---
318
- case 'document':
319
- return this._getScopedDocument();
320
-
321
- // --- Storage scoping ---
322
- case 'localStorage':
323
- return this._getScopedStorage('local');
324
- case 'sessionStorage':
325
- return this._getScopedStorage('session');
326
- }
327
-
328
- return undefined;
329
- }
330
-
331
- // ================================================================
332
- // DOM SCOPING - querySelector searches inside shadow root
333
- // ================================================================
334
-
335
- _getScopedDocument() {
336
- if (this._scopedDocument) return this._scopedDocument;
337
-
338
- const root = this._shadowRoot || this._container;
339
- if (!root) return document; // No container set, pass through
340
-
341
- const self = this;
342
-
343
- this._scopedDocument = new Proxy(document, {
344
- get(target, prop) {
345
- switch (prop) {
346
- case 'querySelector':
347
- return (selector) => root.querySelector(selector);
348
- case 'querySelectorAll':
349
- return (selector) => root.querySelectorAll(selector);
350
- case 'getElementById':
351
- return (id) => root.querySelector(`#${CSS.escape(id)}`);
352
- case 'getElementsByClassName':
353
- return (className) => root.querySelectorAll(`.${CSS.escape(className)}`);
354
- case 'getElementsByTagName':
355
- return (tag) => root.querySelectorAll(tag);
356
-
357
- // Track document event listeners too
358
- case 'addEventListener':
359
- return function(event, handler, options) {
360
- self._eventListeners.push({ target, event, handler, options });
361
- target.addEventListener(event, handler, options);
362
- };
363
- case 'removeEventListener':
364
- return function(event, handler, options) {
365
- self._eventListeners = self._eventListeners.filter(
366
- l => !(l.target === target && l.event === event && l.handler === handler)
367
- );
368
- target.removeEventListener(event, handler, options);
369
- };
370
-
371
- // createElement, createTextNode, etc. - pass through
372
- default: {
373
- const value = target[prop];
374
- if (typeof value === 'function') {
375
- return value.bind(target);
376
- }
377
- return value;
378
- }
379
- }
380
- }
381
- });
382
-
383
- return this._scopedDocument;
384
- }
385
-
386
- // ================================================================
387
- // STORAGE SCOPING - localStorage/sessionStorage with app prefix
388
- // ================================================================
389
-
390
- _getScopedStorage(type) {
391
- const cacheKey = type === 'local' ? '_scopedLocalStorage' : '_scopedSessionStorage';
392
- if (this[cacheKey]) return this[cacheKey];
393
-
394
- const realStorage = type === 'local' ? window.localStorage : window.sessionStorage;
395
- if (!realStorage) return realStorage;
396
-
397
- const prefix = `wu_${this.appName}_`;
398
-
399
- this[cacheKey] = {
400
- getItem(key) {
401
- return realStorage.getItem(prefix + key);
402
- },
403
- setItem(key, value) {
404
- realStorage.setItem(prefix + key, String(value));
405
- },
406
- removeItem(key) {
407
- realStorage.removeItem(prefix + key);
408
- },
409
- clear() {
410
- // Only clear this app's keys
411
- const toRemove = [];
412
- for (let i = 0; i < realStorage.length; i++) {
413
- const k = realStorage.key(i);
414
- if (k && k.startsWith(prefix)) toRemove.push(k);
415
- }
416
- toRemove.forEach(k => realStorage.removeItem(k));
417
- },
418
- key(index) {
419
- let count = 0;
420
- for (let i = 0; i < realStorage.length; i++) {
421
- const k = realStorage.key(i);
422
- if (k && k.startsWith(prefix)) {
423
- if (count === index) return k.slice(prefix.length);
424
- count++;
425
- }
426
- }
427
- return null;
428
- },
429
- get length() {
430
- let count = 0;
431
- for (let i = 0; i < realStorage.length; i++) {
432
- if (realStorage.key(i)?.startsWith(prefix)) count++;
433
- }
434
- return count;
435
- }
436
- };
437
-
438
- return this[cacheKey];
439
- }
440
-
441
- // ================================================================
442
- // UTILITIES
443
- // ================================================================
444
-
445
- _isConstructor(fn) {
446
- try {
447
- return fn.prototype && fn.prototype.constructor === fn;
448
- } catch {
449
- return false;
450
- }
451
- }
452
-
453
- getProxy() {
454
- return this.active ? this.proxy : null;
455
- }
456
-
457
- isActive() {
458
- return this.active;
459
- }
460
-
461
- getStats() {
462
- return {
463
- appName: this.appName,
464
- active: this.active,
465
- patched: this._patched,
466
- modifiedKeys: Array.from(this.modifiedKeys),
467
- isolatedPropsCount: Object.keys(this.fakeWindow).length,
468
- trackedTimers: this._timers.size,
469
- trackedIntervals: this._intervals.size,
470
- trackedRAFs: this._rafs.size,
471
- trackedEventListeners: this._eventListeners.length,
472
- hasContainer: !!this._container,
473
- hasShadowRoot: !!this._shadowRoot
474
- };
475
- }
476
- }
1
+ /**
2
+ * WU-PROXY-SANDBOX: Hardened JavaScript Isolation
3
+ *
4
+ * ES6 Proxy-based sandbox with side-effect tracking:
5
+ * - Timer hijacking (setTimeout, setInterval, requestAnimationFrame)
6
+ * - Event listener tracking (window + document addEventListener)
7
+ * - DOM scoping (querySelector/querySelectorAll → shadow root)
8
+ * - Storage scoping (localStorage/sessionStorage → prefixed keys)
9
+ *
10
+ * All tracked side effects are automatically cleaned up on deactivate().
11
+ */
12
+
13
+ import { logger } from './wu-logger.js';
14
+
15
+ export class WuProxySandbox {
16
+ constructor(appName) {
17
+ this.appName = appName;
18
+ this.proxy = null;
19
+ this.fakeWindow = Object.create(null);
20
+ this.active = false;
21
+ this.modifiedKeys = new Set();
22
+
23
+ // --- Side-effect tracking ---
24
+ this._timers = new Set();
25
+ this._intervals = new Set();
26
+ this._rafs = new Set();
27
+ this._eventListeners = []; // [{target, event, handler, options}]
28
+
29
+ // --- DOM & Storage scoping ---
30
+ this._container = null;
31
+ this._shadowRoot = null;
32
+ this._scopedDocument = null;
33
+ this._scopedLocalStorage = null;
34
+ this._scopedSessionStorage = null;
35
+
36
+ // --- Window patching state ---
37
+ this._patched = false;
38
+ this._originals = null;
39
+ }
40
+
41
+ /**
42
+ * Set the DOM scope for this sandbox.
43
+ * Must be called before activate() for DOM scoping to work.
44
+ * @param {HTMLElement} container - App container element
45
+ * @param {ShadowRoot} shadowRoot - Shadow root containing the container
46
+ */
47
+ setContainer(container, shadowRoot) {
48
+ this._container = container;
49
+ this._shadowRoot = shadowRoot;
50
+ }
51
+
52
+ /**
53
+ * Activate the sandbox. Creates the Proxy and starts tracking.
54
+ * @returns {Proxy} The sandboxed window proxy
55
+ */
56
+ activate() {
57
+ if (this.active) return this.proxy;
58
+
59
+ const self = this;
60
+
61
+ this.proxy = new Proxy(window, {
62
+ get(target, prop) {
63
+ // 1. App's own isolated globals
64
+ if (prop in self.fakeWindow) {
65
+ return self.fakeWindow[prop];
66
+ }
67
+
68
+ // 2. Intercepted APIs
69
+ const intercepted = self._intercept(prop, target);
70
+ if (intercepted !== undefined) {
71
+ return intercepted;
72
+ }
73
+
74
+ // 3. Real window value with correct binding
75
+ const value = target[prop];
76
+ if (typeof value === 'function' && !self._isConstructor(value)) {
77
+ return value.bind(target);
78
+ }
79
+ return value;
80
+ },
81
+
82
+ set(target, prop, value) {
83
+ self.fakeWindow[prop] = value;
84
+ self.modifiedKeys.add(prop);
85
+ return true;
86
+ },
87
+
88
+ has(target, prop) {
89
+ return prop in self.fakeWindow || prop in target;
90
+ },
91
+
92
+ deleteProperty(target, prop) {
93
+ if (prop in self.fakeWindow) {
94
+ delete self.fakeWindow[prop];
95
+ self.modifiedKeys.delete(prop);
96
+ return true;
97
+ }
98
+ return false;
99
+ }
100
+ });
101
+
102
+ this.active = true;
103
+ logger.wuDebug(`[ProxySandbox] Activated for ${this.appName}`);
104
+ return this.proxy;
105
+ }
106
+
107
+ /**
108
+ * Deactivate the sandbox. Cleans up ALL tracked side effects.
109
+ */
110
+ deactivate() {
111
+ if (!this.active) return;
112
+
113
+ // Unpatch window if patched
114
+ this.unpatchWindow();
115
+
116
+ // --- Clean timers ---
117
+ for (const id of this._timers) {
118
+ try { clearTimeout(id); } catch {}
119
+ }
120
+ for (const id of this._intervals) {
121
+ try { clearInterval(id); } catch {}
122
+ }
123
+ for (const id of this._rafs) {
124
+ try { cancelAnimationFrame(id); } catch {}
125
+ }
126
+
127
+ const timerCount = this._timers.size + this._intervals.size + this._rafs.size;
128
+ this._timers.clear();
129
+ this._intervals.clear();
130
+ this._rafs.clear();
131
+
132
+ // --- Clean event listeners ---
133
+ const listenerCount = this._eventListeners.length;
134
+ for (const { target, event, handler, options } of this._eventListeners) {
135
+ try { target.removeEventListener(event, handler, options); } catch {}
136
+ }
137
+ this._eventListeners = [];
138
+
139
+ // --- Clean namespace ---
140
+ this.fakeWindow = Object.create(null);
141
+ this.modifiedKeys.clear();
142
+ this._scopedDocument = null;
143
+ this._scopedLocalStorage = null;
144
+ this._scopedSessionStorage = null;
145
+ this.proxy = null;
146
+ this.active = false;
147
+
148
+ if (timerCount > 0 || listenerCount > 0) {
149
+ logger.wuDebug(
150
+ `[ProxySandbox] ${this.appName} cleanup: ${timerCount} timers, ${listenerCount} listeners`
151
+ );
152
+ }
153
+ logger.wuDebug(`[ProxySandbox] Deactivated for ${this.appName}`);
154
+ }
155
+
156
+ // ================================================================
157
+ // WINDOW PATCHING - patches real window APIs during module loading
158
+ // ================================================================
159
+
160
+ /**
161
+ * Patch real window APIs to track side effects from global code.
162
+ * Call before loading app module, unpatch after.
163
+ *
164
+ * IMPORTANT: Uses closure over originals so patched functions remain
165
+ * valid even after unpatchWindow() — prevents crashes when frameworks
166
+ * (React 19, etc.) cache references to patched setTimeout during import.
167
+ */
168
+ patchWindow() {
169
+ if (this._patched) return;
170
+
171
+ const self = this;
172
+
173
+ // Capture originals in a local closure that survives unpatch
174
+ const originals = {
175
+ setTimeout: window.setTimeout,
176
+ clearTimeout: window.clearTimeout,
177
+ setInterval: window.setInterval,
178
+ clearInterval: window.clearInterval,
179
+ requestAnimationFrame: window.requestAnimationFrame,
180
+ cancelAnimationFrame: window.cancelAnimationFrame,
181
+ addEventListener: window.addEventListener,
182
+ removeEventListener: window.removeEventListener
183
+ };
184
+
185
+ // Store reference (used by unpatchWindow to restore)
186
+ this._originals = originals;
187
+
188
+ // Patch timers — closure captures `originals`, not `self._originals`
189
+ window.setTimeout = function(fn, delay, ...args) {
190
+ const id = originals.setTimeout.call(window, fn, delay, ...args);
191
+ if (self._patched) self._timers.add(id);
192
+ return id;
193
+ };
194
+ window.clearTimeout = function(id) {
195
+ self._timers.delete(id);
196
+ return originals.clearTimeout.call(window, id);
197
+ };
198
+ window.setInterval = function(fn, delay, ...args) {
199
+ const id = originals.setInterval.call(window, fn, delay, ...args);
200
+ if (self._patched) self._intervals.add(id);
201
+ return id;
202
+ };
203
+ window.clearInterval = function(id) {
204
+ self._intervals.delete(id);
205
+ return originals.clearInterval.call(window, id);
206
+ };
207
+ window.requestAnimationFrame = function(fn) {
208
+ const id = originals.requestAnimationFrame.call(window, fn);
209
+ if (self._patched) self._rafs.add(id);
210
+ return id;
211
+ };
212
+ window.cancelAnimationFrame = function(id) {
213
+ self._rafs.delete(id);
214
+ return originals.cancelAnimationFrame.call(window, id);
215
+ };
216
+
217
+ // Patch event listeners
218
+ window.addEventListener = function(event, handler, options) {
219
+ if (self._patched) self._eventListeners.push({ target: window, event, handler, options });
220
+ return originals.addEventListener.call(window, event, handler, options);
221
+ };
222
+ window.removeEventListener = function(event, handler, options) {
223
+ self._eventListeners = self._eventListeners.filter(
224
+ l => !(l.target === window && l.event === event && l.handler === handler)
225
+ );
226
+ return originals.removeEventListener.call(window, event, handler, options);
227
+ };
228
+
229
+ this._patched = true;
230
+ logger.wuDebug(`[ProxySandbox] Window patched for ${this.appName}`);
231
+ }
232
+
233
+ /**
234
+ * Restore original window APIs.
235
+ * Safe: patched functions still work via closure even after restore.
236
+ */
237
+ unpatchWindow() {
238
+ if (!this._patched || !this._originals) return;
239
+
240
+ window.setTimeout = this._originals.setTimeout;
241
+ window.clearTimeout = this._originals.clearTimeout;
242
+ window.setInterval = this._originals.setInterval;
243
+ window.clearInterval = this._originals.clearInterval;
244
+ window.requestAnimationFrame = this._originals.requestAnimationFrame;
245
+ window.cancelAnimationFrame = this._originals.cancelAnimationFrame;
246
+ window.addEventListener = this._originals.addEventListener;
247
+ window.removeEventListener = this._originals.removeEventListener;
248
+
249
+ // NOTE: Do NOT null _originals — patched closures may still reference
250
+ // the sandbox instance (e.g. React scheduler caches setTimeout).
251
+ // The closure uses `originals` (local const), not `this._originals`.
252
+ this._patched = false;
253
+ logger.wuDebug(`[ProxySandbox] Window unpatched for ${this.appName}`);
254
+ }
255
+
256
+ // ================================================================
257
+ // PROXY INTERCEPTS - for code running through the proxy
258
+ // ================================================================
259
+
260
+ /**
261
+ * Intercept property access on the proxy.
262
+ * Returns wrapped API or undefined to fall through.
263
+ */
264
+ _intercept(prop, target) {
265
+ const self = this;
266
+
267
+ switch (prop) {
268
+ // --- Timer hijacking ---
269
+ case 'setTimeout':
270
+ return function(fn, delay, ...args) {
271
+ const id = target.setTimeout(fn, delay, ...args);
272
+ self._timers.add(id);
273
+ return id;
274
+ };
275
+ case 'clearTimeout':
276
+ return function(id) {
277
+ self._timers.delete(id);
278
+ target.clearTimeout(id);
279
+ };
280
+ case 'setInterval':
281
+ return function(fn, delay, ...args) {
282
+ const id = target.setInterval(fn, delay, ...args);
283
+ self._intervals.add(id);
284
+ return id;
285
+ };
286
+ case 'clearInterval':
287
+ return function(id) {
288
+ self._intervals.delete(id);
289
+ target.clearInterval(id);
290
+ };
291
+ case 'requestAnimationFrame':
292
+ return function(fn) {
293
+ const id = target.requestAnimationFrame(fn);
294
+ self._rafs.add(id);
295
+ return id;
296
+ };
297
+ case 'cancelAnimationFrame':
298
+ return function(id) {
299
+ self._rafs.delete(id);
300
+ target.cancelAnimationFrame(id);
301
+ };
302
+
303
+ // --- Event listener tracking ---
304
+ case 'addEventListener':
305
+ return function(event, handler, options) {
306
+ self._eventListeners.push({ target, event, handler, options });
307
+ target.addEventListener(event, handler, options);
308
+ };
309
+ case 'removeEventListener':
310
+ return function(event, handler, options) {
311
+ self._eventListeners = self._eventListeners.filter(
312
+ l => !(l.target === target && l.event === event && l.handler === handler)
313
+ );
314
+ target.removeEventListener(event, handler, options);
315
+ };
316
+
317
+ // --- DOM scoping ---
318
+ case 'document':
319
+ return this._getScopedDocument();
320
+
321
+ // --- Storage scoping ---
322
+ case 'localStorage':
323
+ return this._getScopedStorage('local');
324
+ case 'sessionStorage':
325
+ return this._getScopedStorage('session');
326
+ }
327
+
328
+ return undefined;
329
+ }
330
+
331
+ // ================================================================
332
+ // DOM SCOPING - querySelector searches inside shadow root
333
+ // ================================================================
334
+
335
+ _getScopedDocument() {
336
+ if (this._scopedDocument) return this._scopedDocument;
337
+
338
+ const root = this._shadowRoot || this._container;
339
+ if (!root) return document; // No container set, pass through
340
+
341
+ const self = this;
342
+
343
+ this._scopedDocument = new Proxy(document, {
344
+ get(target, prop) {
345
+ switch (prop) {
346
+ case 'querySelector':
347
+ return (selector) => root.querySelector(selector);
348
+ case 'querySelectorAll':
349
+ return (selector) => root.querySelectorAll(selector);
350
+ case 'getElementById':
351
+ return (id) => root.querySelector(`#${CSS.escape(id)}`);
352
+ case 'getElementsByClassName':
353
+ return (className) => root.querySelectorAll(`.${CSS.escape(className)}`);
354
+ case 'getElementsByTagName':
355
+ return (tag) => root.querySelectorAll(tag);
356
+
357
+ // Track document event listeners too
358
+ case 'addEventListener':
359
+ return function(event, handler, options) {
360
+ self._eventListeners.push({ target, event, handler, options });
361
+ target.addEventListener(event, handler, options);
362
+ };
363
+ case 'removeEventListener':
364
+ return function(event, handler, options) {
365
+ self._eventListeners = self._eventListeners.filter(
366
+ l => !(l.target === target && l.event === event && l.handler === handler)
367
+ );
368
+ target.removeEventListener(event, handler, options);
369
+ };
370
+
371
+ // createElement, createTextNode, etc. - pass through
372
+ default: {
373
+ const value = target[prop];
374
+ if (typeof value === 'function') {
375
+ return value.bind(target);
376
+ }
377
+ return value;
378
+ }
379
+ }
380
+ }
381
+ });
382
+
383
+ return this._scopedDocument;
384
+ }
385
+
386
+ // ================================================================
387
+ // STORAGE SCOPING - localStorage/sessionStorage with app prefix
388
+ // ================================================================
389
+
390
+ _getScopedStorage(type) {
391
+ const cacheKey = type === 'local' ? '_scopedLocalStorage' : '_scopedSessionStorage';
392
+ if (this[cacheKey]) return this[cacheKey];
393
+
394
+ const realStorage = type === 'local' ? window.localStorage : window.sessionStorage;
395
+ if (!realStorage) return realStorage;
396
+
397
+ const prefix = `wu_${this.appName}_`;
398
+
399
+ this[cacheKey] = {
400
+ getItem(key) {
401
+ return realStorage.getItem(prefix + key);
402
+ },
403
+ setItem(key, value) {
404
+ realStorage.setItem(prefix + key, String(value));
405
+ },
406
+ removeItem(key) {
407
+ realStorage.removeItem(prefix + key);
408
+ },
409
+ clear() {
410
+ // Only clear this app's keys
411
+ const toRemove = [];
412
+ for (let i = 0; i < realStorage.length; i++) {
413
+ const k = realStorage.key(i);
414
+ if (k && k.startsWith(prefix)) toRemove.push(k);
415
+ }
416
+ toRemove.forEach(k => realStorage.removeItem(k));
417
+ },
418
+ key(index) {
419
+ let count = 0;
420
+ for (let i = 0; i < realStorage.length; i++) {
421
+ const k = realStorage.key(i);
422
+ if (k && k.startsWith(prefix)) {
423
+ if (count === index) return k.slice(prefix.length);
424
+ count++;
425
+ }
426
+ }
427
+ return null;
428
+ },
429
+ get length() {
430
+ let count = 0;
431
+ for (let i = 0; i < realStorage.length; i++) {
432
+ if (realStorage.key(i)?.startsWith(prefix)) count++;
433
+ }
434
+ return count;
435
+ }
436
+ };
437
+
438
+ return this[cacheKey];
439
+ }
440
+
441
+ // ================================================================
442
+ // UTILITIES
443
+ // ================================================================
444
+
445
+ _isConstructor(fn) {
446
+ try {
447
+ return fn.prototype && fn.prototype.constructor === fn;
448
+ } catch {
449
+ return false;
450
+ }
451
+ }
452
+
453
+ getProxy() {
454
+ return this.active ? this.proxy : null;
455
+ }
456
+
457
+ isActive() {
458
+ return this.active;
459
+ }
460
+
461
+ getStats() {
462
+ return {
463
+ appName: this.appName,
464
+ active: this.active,
465
+ patched: this._patched,
466
+ modifiedKeys: Array.from(this.modifiedKeys),
467
+ isolatedPropsCount: Object.keys(this.fakeWindow).length,
468
+ trackedTimers: this._timers.size,
469
+ trackedIntervals: this._intervals.size,
470
+ trackedRAFs: this._rafs.size,
471
+ trackedEventListeners: this._eventListeners.length,
472
+ hasContainer: !!this._container,
473
+ hasShadowRoot: !!this._shadowRoot
474
+ };
475
+ }
476
+ }