@dynamic-labs-wallet/browser-wallet-client 0.0.351 → 0.0.352

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.
package/index.cjs CHANGED
@@ -237,19 +237,136 @@ const setupMessageTransportBridge = (messageTransport$1, iframe, iframeOrigin)=>
237
237
  };
238
238
  };
239
239
 
240
- const FRAME_ANCESTORS_QUERY_PARAM = 'frameAncestors';
241
- /**
242
- * Whether the given iframe element is mounted and accessible.
243
- *
244
- * - contentWindow becomes null once the iframe is removed from the DOM.
245
- * - isConnected returns false when the element has been detached. Some test
246
- * environments leave it undefined, which we treat as alive — falling back
247
- * to "absence === alive" matches the production behavior on real
248
- * HTMLIFrameElement.
249
- */ const isIframeAlive = (iframe)=>{
250
- var _iframe_isConnected;
251
- return !!(iframe == null ? void 0 : iframe.contentWindow) && ((_iframe_isConnected = iframe.isConnected) != null ? _iframe_isConnected : true);
240
+ const createIframeWaasSDKContainer = ()=>{
241
+ let iframe = null;
242
+ const messageHandlers = new Set();
243
+ const errorHandlers = new Set();
244
+ let windowListenerAttached = false;
245
+ let cspListenerAttached = false;
246
+ const windowMessageListener = (event)=>{
247
+ if (event.source !== (iframe == null ? void 0 : iframe.contentWindow)) return;
248
+ // Defense in depth: verify event.origin matches the iframe's URL origin.
249
+ // The source check alone is insufficient — a cross-origin navigation can
250
+ // keep contentWindow stable while changing event.origin to an attacker.
251
+ let expectedOrigin;
252
+ try {
253
+ expectedOrigin = new URL(iframe.src).origin;
254
+ } catch (e) {
255
+ return;
256
+ }
257
+ if (event.origin !== expectedOrigin) return;
258
+ // Only forward payloads that match a known shape so a compromised iframe
259
+ // cannot inject arbitrary data into subscribers. Two shapes are accepted:
260
+ // 1. Handshake: a non-empty string (e.g. `iframe-ready-${instanceId}`).
261
+ // 2. Transport message: a plain object explicitly tagged `origin: 'webview'`.
262
+ const { data } = event;
263
+ const isHandshake = typeof data === 'string' && data.length > 0;
264
+ const isTransportMessage = typeof data === 'object' && data !== null && !Array.isArray(data) && 'origin' in data && data.origin === 'webview';
265
+ if (!isHandshake && !isTransportMessage) return;
266
+ for (const handler of messageHandlers){
267
+ handler(data);
268
+ }
269
+ };
270
+ const fireError = (error)=>{
271
+ for (const handler of errorHandlers){
272
+ handler(error);
273
+ }
274
+ };
275
+ const cspViolationListener = (event)=>{
276
+ var _event_blockedURI, _event_violatedDirective, _event_violatedDirective1;
277
+ const iframeOrigin = (iframe == null ? void 0 : iframe.src) ? new URL(iframe.src).origin : undefined;
278
+ const affectsIframe = iframeOrigin && ((_event_blockedURI = event.blockedURI) == null ? void 0 : _event_blockedURI.includes(iframeOrigin)) || ((_event_violatedDirective = event.violatedDirective) == null ? void 0 : _event_violatedDirective.includes('frame-src')) || ((_event_violatedDirective1 = event.violatedDirective) == null ? void 0 : _event_violatedDirective1.includes('child-src'));
279
+ if (!affectsIframe) return;
280
+ logger.error('CSP violation detected that may affect iframe loading:', {
281
+ blockedURI: event.blockedURI,
282
+ violatedDirective: event.violatedDirective,
283
+ originalPolicy: event.originalPolicy,
284
+ sourceFile: event.sourceFile,
285
+ lineNumber: event.lineNumber,
286
+ columnNumber: event.columnNumber,
287
+ iframeOrigin
288
+ });
289
+ };
290
+ return {
291
+ async setUrl (url) {
292
+ iframe = document.createElement('iframe');
293
+ iframe.style.display = 'none';
294
+ iframe.style.position = 'fixed';
295
+ iframe.style.top = '0';
296
+ iframe.style.left = '0';
297
+ iframe.style.width = '0';
298
+ iframe.style.height = '0';
299
+ iframe.style.border = 'none';
300
+ iframe.style.pointerEvents = 'none';
301
+ iframe.setAttribute('title', 'Dynamic Wallet Iframe');
302
+ iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
303
+ iframe.setAttribute('referrerpolicy', 'origin');
304
+ iframe.onerror = (error)=>{
305
+ fireError(error);
306
+ };
307
+ if (!cspListenerAttached) {
308
+ document.addEventListener('securitypolicyviolation', cspViolationListener);
309
+ cspListenerAttached = true;
310
+ }
311
+ iframe.src = url;
312
+ document.body.appendChild(iframe);
313
+ },
314
+ sendMessage (data) {
315
+ if (!(iframe == null ? void 0 : iframe.contentWindow)) {
316
+ throw new Error('Cannot send message: iframe not loaded');
317
+ }
318
+ const origin = new URL(iframe.src).origin;
319
+ try {
320
+ iframe.contentWindow.postMessage(data, origin);
321
+ } catch (error) {
322
+ // This catches "Attempt to postMessage on disconnected port" errors
323
+ // which occur when the iframe has been navigated away or destroyed.
324
+ logger.error('Failed to post message to iframe:', error);
325
+ throw error;
326
+ }
327
+ },
328
+ onMessage (handler) {
329
+ messageHandlers.add(handler);
330
+ if (!windowListenerAttached) {
331
+ window.addEventListener('message', windowMessageListener);
332
+ windowListenerAttached = true;
333
+ }
334
+ return ()=>{
335
+ messageHandlers.delete(handler);
336
+ };
337
+ },
338
+ onError (handler) {
339
+ errorHandlers.add(handler);
340
+ return ()=>{
341
+ errorHandlers.delete(handler);
342
+ };
343
+ },
344
+ isAlive () {
345
+ var _iframe_isConnected;
346
+ // contentWindow goes null when the iframe is removed from the DOM or its
347
+ // page is torn down (relogin, RN modal unmount, etc.). isConnected falls
348
+ // back to true on jsdom where the property is absent so the production
349
+ // contract still holds.
350
+ return !!(iframe == null ? void 0 : iframe.contentWindow) && ((_iframe_isConnected = iframe.isConnected) != null ? _iframe_isConnected : true);
351
+ },
352
+ destroy () {
353
+ if (windowListenerAttached) {
354
+ window.removeEventListener('message', windowMessageListener);
355
+ windowListenerAttached = false;
356
+ }
357
+ if (cspListenerAttached) {
358
+ document.removeEventListener('securitypolicyviolation', cspViolationListener);
359
+ cspListenerAttached = false;
360
+ }
361
+ messageHandlers.clear();
362
+ errorHandlers.clear();
363
+ iframe == null ? void 0 : iframe.remove();
364
+ iframe = null;
365
+ }
366
+ };
252
367
  };
368
+
369
+ const FRAME_ANCESTORS_QUERY_PARAM = 'frameAncestors';
253
370
  /**
254
371
  * Builds the iframe message transport with the recovery + block decorators.
255
372
  *
@@ -300,6 +417,96 @@ class IframeManager {
300
417
  throw error;
301
418
  }
302
419
  }
420
+ buildIframeUrlSearchParams() {
421
+ var _this_instanceId, _this_sdkVersion, _this_baseClientKeysharesRelayApiUrl;
422
+ const params = new URLSearchParams({
423
+ instanceId: (_this_instanceId = this.instanceId) != null ? _this_instanceId : '',
424
+ hostOrigin: window.location.origin,
425
+ environmentId: this.environmentId,
426
+ baseApiUrl: this.baseApiUrl,
427
+ baseMPCRelayApiUrl: this.baseMPCRelayApiUrl,
428
+ sdkVersion: (_this_sdkVersion = this.sdkVersion) != null ? _this_sdkVersion : '',
429
+ debug: String(this.debug),
430
+ baseClientKeysharesRelayApiUrl: (_this_baseClientKeysharesRelayApiUrl = this.baseClientKeysharesRelayApiUrl) != null ? _this_baseClientKeysharesRelayApiUrl : '',
431
+ secureStorage: this.secureStorage ? 'true' : ''
432
+ });
433
+ const seenAdditional = new Set();
434
+ for (const raw of this.additionalTrustedOrigins){
435
+ const trimmed = raw.trim();
436
+ if (!trimmed) {
437
+ continue;
438
+ }
439
+ if (trimmed.length > IframeManager.maxTrustedOriginStringLength) {
440
+ this.logger.warn('Skipping additionalTrustedOrigin (exceeds max length)');
441
+ continue;
442
+ }
443
+ try {
444
+ const u = new URL(trimmed);
445
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
446
+ this.logger.warn('Skipping additionalTrustedOrigin (unsupported scheme)', {
447
+ trimmed
448
+ });
449
+ continue;
450
+ }
451
+ const origin = u.origin;
452
+ if (seenAdditional.has(origin)) {
453
+ continue;
454
+ }
455
+ if (seenAdditional.size >= IframeManager.maxAdditionalTrustedOrigins) {
456
+ this.logger.warn('additionalTrustedOrigin limit reached, extra entries ignored');
457
+ break;
458
+ }
459
+ seenAdditional.add(origin);
460
+ params.append(FRAME_ANCESTORS_QUERY_PARAM, origin);
461
+ } catch (e) {
462
+ this.logger.warn('Skipping invalid additionalTrustedOrigin', {
463
+ trimmed
464
+ });
465
+ }
466
+ }
467
+ return params;
468
+ }
469
+ /**
470
+ * Build the URL that the transport provider should load.
471
+ */ buildIframeUrl() {
472
+ const params = this.buildIframeUrlSearchParams();
473
+ return `${this.iframeDomain}/waas-v1/${this.environmentId}?${params.toString()}`;
474
+ }
475
+ /**
476
+ * Set up the bridge between the MessageTransport and the WaasSDKContainer.
477
+ * - Host-origin messages from the transport are forwarded to the sandbox host via sendMessage.
478
+ * - Messages received from the sandbox host are parsed and emitted into the transport.
479
+ *
480
+ * Returns a teardown function so callers can detach listeners when the
481
+ * sandbox host is rebuilt (needed by the recovery flow — the transport
482
+ * survives the rebuild and would otherwise accumulate stale listeners).
483
+ */ setupWaasSDKContainerBridge(transport, waasSDKContainer) {
484
+ // Forward outgoing host messages to the sandbox host
485
+ const transportListener = (message)=>{
486
+ if (message.origin === 'host') {
487
+ waasSDKContainer.sendMessage(message);
488
+ }
489
+ };
490
+ transport.on(transportListener);
491
+ // Forward incoming messages from the sandbox host to the transport
492
+ const unsubscribeFromWaasSDKContainer = waasSDKContainer.onMessage((data)=>{
493
+ if (typeof data !== 'object' || data === null) return;
494
+ if (!('origin' in data) || data.origin !== 'webview') return;
495
+ try {
496
+ const parsed = messageTransport.parseMessageTransportData(data);
497
+ if (!parsed) return;
498
+ transport.emit(parsed);
499
+ } catch (error) {
500
+ if (!(error instanceof SyntaxError)) {
501
+ this.logger.error('Error handling incoming message:', error);
502
+ }
503
+ }
504
+ });
505
+ return ()=>{
506
+ transport.off(transportListener);
507
+ unsubscribeFromWaasSDKContainer();
508
+ };
509
+ }
303
510
  /**
304
511
  * initialize the message transport after iframe is successfully loaded
305
512
  */ async initializeMessageTransport() {
@@ -310,10 +517,16 @@ class IframeManager {
310
517
  await this.initializeIframeCommunication();
311
518
  const transport = createIframeMessageTransport();
312
519
  this.messageTransport = transport;
313
- if (!this.iframe) {
314
- throw new Error('Iframe not available');
520
+ var _this_waasSDKContainer;
521
+ const waasSDKContainer = (_this_waasSDKContainer = this.waasSDKContainer) != null ? _this_waasSDKContainer : IframeManager.sharedWaasSDKContainer;
522
+ if (waasSDKContainer) {
523
+ this.cleanupBridge = this.setupWaasSDKContainerBridge(transport, waasSDKContainer);
524
+ } else if (this.iframe) {
525
+ // Fallback for the display container flow which still uses raw iframes
526
+ this.cleanupBridge = setupMessageTransportBridge(this.messageTransport, this.iframe, this.iframeDomain);
527
+ } else {
528
+ throw new Error('No sandbox host or iframe available');
315
529
  }
316
- this.cleanupBridge = setupMessageTransportBridge(this.messageTransport, this.iframe, this.iframeDomain);
317
530
  this.iframeMessageHandler = new iframeMessageHandler(this.messageTransport);
318
531
  // Set up request channel to handle messages from iframe (for secureStorage and getSignedSessionId)
319
532
  if (this.secureStorage || this.getSignedSessionIdCallback) {
@@ -339,51 +552,60 @@ class IframeManager {
339
552
  await this.initAuthToken();
340
553
  }
341
554
  /**
342
- * Rebuild the iframe and its bridge while preserving the message transport
555
+ * Rebuild the sandbox host and its bridge while preserving the message transport
343
556
  * (which owns the request channel's retry timer and recovery manager).
344
557
  *
345
558
  * Triggered by the request channel's recovery flow when an outgoing message
346
559
  * goes 5s without an ack — typically because the iframe was torn down by a
347
560
  * relogin or RN modal unmount and the host still holds a stale reference.
348
561
  */ async recoverIframe(transport) {
349
- // If sharedIframe is still alive (contentWindow + connected), the request
350
- // channel's 5s timeout fired for a *non-iframe-death* reason slow
351
- // server, overloaded MPC ceremony, etc. Rebuilding the iframe in that
352
- // case actively breaks things: it removes a healthy iframe out from
353
- // under in-flight operations and amplifies retries when the next
354
- // message also times out → endless cascade.
562
+ var _IframeManager_sharedWaasSDKContainer;
563
+ // If the shared container is still alive, the request channel's 5s timeout
564
+ // fired for a *non-death* reason — slow server, overloaded MPC ceremony,
565
+ // etc. Rebuilding in that case actively breaks things: it tears a healthy
566
+ // container out from under in-flight operations and amplifies retries when
567
+ // the next message also times out → endless cascade.
355
568
  //
356
569
  // Just unblock the transport (the request channel's own retry will
357
- // re-send the message on the same live iframe) and bail out. Only the
358
- // real dead-iframe case below falls through to the rebuild.
359
- if (isIframeAlive(IframeManager.sharedIframe)) {
360
- this.logger.info('(recoverIframe) Iframe still alive — request-channel timeout, skipping rebuild');
361
- this.iframe = IframeManager.sharedIframe;
570
+ // re-send the message on the same live container) and bail out. Only the
571
+ // real dead-container case below falls through to the rebuild.
572
+ if ((_IframeManager_sharedWaasSDKContainer = IframeManager.sharedWaasSDKContainer) == null ? void 0 : _IframeManager_sharedWaasSDKContainer.isAlive()) {
573
+ this.logger.info('(recoverIframe) Container still alive — request-channel timeout, skipping rebuild');
574
+ this.waasSDKContainer = IframeManager.sharedWaasSDKContainer;
362
575
  transport.unblock();
363
576
  return;
364
577
  }
365
- // Detach listeners attached to the dead iframe.
578
+ // Detach listeners attached to the dead container, then destroy it.
579
+ // The transport, iframeMessageHandler, and iframeRequestChannel are
580
+ // intentionally kept — they're tied to the request channel's in-flight
581
+ // retry that we want to flush after rebuild.
366
582
  this.cleanupBridge == null ? void 0 : this.cleanupBridge.call(this);
367
583
  this.cleanupBridge = null;
368
- this.iframe = null;
369
- // Defer the DOM rebuild + concurrency control to loadIframe(): the first
370
- // chain to land here will fall through to the remount branch (sharedIframe
371
- // is dead, iframeLoadPromise is null), set iframeLoadPromise, and start
372
- // the mount. Subsequent chains' loadIframe() calls during this window see
373
- // iframeLoadPromise set and await the same load — concurrent rebuilds
374
- // collapse for free, no extra static needed.
584
+ if (IframeManager.sharedWaasSDKContainer) {
585
+ IframeManager.sharedWaasSDKContainer.destroy();
586
+ IframeManager.sharedWaasSDKContainer = null;
587
+ }
588
+ IframeManager.iframeLoadPromise = null;
589
+ this.waasSDKContainer = null;
590
+ // Defer the rebuild + concurrency control to loadIframe(): the first
591
+ // chain to land here will fall through to the remount branch (shared
592
+ // container is dead, iframeLoadPromise is null), set iframeLoadPromise,
593
+ // and start the mount. Subsequent chains' loadIframe() calls during this
594
+ // window see iframeLoadPromise set and await the same load — concurrent
595
+ // rebuilds collapse for free, no extra static needed.
375
596
  //
376
597
  // The transport, iframeMessageHandler, and iframeRequestChannel are
377
598
  // intentionally kept — they're tied to the request channel's in-flight
378
599
  // retry that we want to flush after rebuild.
379
- this.logger.info('(recoverIframe) Rebuilding shared iframe');
600
+ this.logger.info('(recoverIframe) Rebuilding shared container');
380
601
  await this.loadIframe();
381
- this.iframe = IframeManager.sharedIframe;
382
- if (!this.iframe) {
383
- throw new Error('Failed to mount fresh iframe during recovery');
602
+ var _this_waasSDKContainer;
603
+ const waasSDKContainer = (_this_waasSDKContainer = this.waasSDKContainer) != null ? _this_waasSDKContainer : IframeManager.sharedWaasSDKContainer;
604
+ if (!waasSDKContainer) {
605
+ throw new Error('Failed to mount fresh container during recovery');
384
606
  }
385
- this.cleanupBridge = setupMessageTransportBridge(transport, this.iframe, this.iframeDomain);
386
- // Re-send auth token to the new iframe before the queued retry flushes.
607
+ this.cleanupBridge = this.setupWaasSDKContainerBridge(transport, waasSDKContainer);
608
+ // Re-send auth token to the new sandbox host before the queued retry flushes.
387
609
  // sendAuthToken bypasses the block (see bypassBlockIf in
388
610
  // createIframeMessageTransport), so this resolves before unblock() runs
389
611
  // and the queued operation hits an authenticated iframe.
@@ -461,44 +683,52 @@ class IframeManager {
461
683
  }
462
684
  }
463
685
  /**
464
- * Reset the shared iframe and iframe load promise, and iframe instance count
465
- */ async resetSharedIframe() {
686
+ * Reset the shared provider and iframe load promise, and iframe instance count
687
+ */ resetSharedWaasSDKContainer() {
466
688
  this.cleanupBridge == null ? void 0 : this.cleanupBridge.call(this);
467
689
  this.cleanupBridge = null;
468
- IframeManager.sharedIframe = null;
690
+ if (IframeManager.sharedWaasSDKContainer) {
691
+ IframeManager.sharedWaasSDKContainer.destroy();
692
+ IframeManager.sharedWaasSDKContainer = null;
693
+ }
469
694
  IframeManager.iframeInstanceCount = 0;
470
695
  IframeManager.iframeLoadPromise = null;
471
- this.iframe = null;
696
+ this.waasSDKContainer = null;
472
697
  this.iframeMessageHandler = null;
473
698
  this.messageTransport = null;
474
699
  // Double the timeout and cap at 60 seconds to give more time for slow networks
475
700
  IframeManager.iframeLoadTimeout = Math.min(IframeManager.iframeLoadTimeout * 2, 60000);
476
701
  }
477
702
  async loadIframe() {
478
- var // sharedIframe is dead (or never existed) and no load is in flight.
479
- // Clear the stale corpse so createIframeLoadPromise mounts a fresh one.
480
- _IframeManager_sharedIframe;
481
- // If sharedIframe is still alive (contentWindow + connected), reuse it.
482
- // The alive check (not just `if sharedIframe`) is what lets recoverIframe
483
- // simply call loadIframe again on a dead iframe: we fall through to the
484
- // remount branch below instead of adopting the corpse.
485
- if (isIframeAlive(IframeManager.sharedIframe)) {
486
- this.assignExistingIframe();
703
+ var _IframeManager_sharedWaasSDKContainer;
704
+ // If the shared container is still alive, reuse it. The alive check (not
705
+ // just `if sharedWaasSDKContainer`) is what lets recoverIframe simply call
706
+ // loadIframe again on a dead container: we fall through to the remount
707
+ // branch below instead of adopting the corpse.
708
+ if ((_IframeManager_sharedWaasSDKContainer = IframeManager.sharedWaasSDKContainer) == null ? void 0 : _IframeManager_sharedWaasSDKContainer.isAlive()) {
709
+ this.waasSDKContainer = IframeManager.sharedWaasSDKContainer;
710
+ IframeManager.iframeInstanceCount++;
487
711
  return Promise.resolve();
488
712
  }
489
713
  // If a load is in progress, wait for it, then assign
490
714
  if (IframeManager.iframeLoadPromise) {
491
715
  return IframeManager.iframeLoadPromise.then(()=>{
492
- this.assignExistingIframe();
716
+ this.waasSDKContainer = IframeManager.sharedWaasSDKContainer;
717
+ IframeManager.iframeInstanceCount++;
493
718
  });
494
719
  }
495
- (_IframeManager_sharedIframe = IframeManager.sharedIframe) == null ? void 0 : _IframeManager_sharedIframe.remove();
496
- IframeManager.sharedIframe = null;
497
- const loadPromise = IframeManager.iframeLoadPromise = this.createIframeLoadPromise();
720
+ // sharedWaasSDKContainer is dead (or never existed) and no load is in
721
+ // flight. Destroy the stale corpse so createWaasSDKContainerLoadPromise
722
+ // mounts a fresh one.
723
+ if (IframeManager.sharedWaasSDKContainer) {
724
+ IframeManager.sharedWaasSDKContainer.destroy();
725
+ IframeManager.sharedWaasSDKContainer = null;
726
+ }
727
+ const loadPromise = IframeManager.iframeLoadPromise = this.createWaasSDKContainerLoadPromise();
498
728
  // Clear iframeLoadPromise once the load settles so the "in flight" branch
499
729
  // above only fires while a load is actually pending. Otherwise it would
500
730
  // keep returning a resolved promise pointing at the (potentially later
501
- // dead) iframe, and the next caller would adopt a corpse without ever
731
+ // dead) container, and the next caller would adopt a corpse without ever
502
732
  // taking the remount branch.
503
733
  //
504
734
  // The trailing .catch swallows the rejection that finally re-throws —
@@ -514,95 +744,63 @@ class IframeManager {
514
744
  });
515
745
  return loadPromise;
516
746
  }
517
- assignExistingIframe() {
518
- this.iframe = IframeManager.sharedIframe;
519
- IframeManager.iframeInstanceCount++;
520
- }
521
- createIframeLoadPromise() {
747
+ createWaasSDKContainerLoadPromise() {
522
748
  return new Promise((resolve, reject)=>{
523
- // Listen for CSP violations that might block the iframe
524
- const cspViolationListener = (event)=>{
525
- var _event_blockedURI, _event_violatedDirective, _event_violatedDirective1;
526
- if (this.iframeDomain && ((_event_blockedURI = event.blockedURI) == null ? void 0 : _event_blockedURI.includes(this.iframeDomain)) || ((_event_violatedDirective = event.violatedDirective) == null ? void 0 : _event_violatedDirective.includes('frame-src')) || ((_event_violatedDirective1 = event.violatedDirective) == null ? void 0 : _event_violatedDirective1.includes('child-src'))) {
527
- this.logger.error('CSP violation detected that may affect iframe loading:', {
528
- blockedURI: event.blockedURI,
529
- violatedDirective: event.violatedDirective,
530
- originalPolicy: event.originalPolicy,
531
- sourceFile: event.sourceFile,
532
- lineNumber: event.lineNumber,
533
- columnNumber: event.columnNumber,
534
- iframeDomain: this.iframeDomain
535
- });
536
- }
537
- };
538
- document.addEventListener('securitypolicyviolation', cspViolationListener);
539
- const cleanupCspListener = ()=>{
540
- document.removeEventListener('securitypolicyviolation', cspViolationListener);
541
- };
542
749
  const attemptLoad = ()=>{
543
750
  IframeManager.iframeLoadAttempts++;
544
751
  this.logger.debug(`Loading iframe for waas wallet client... (attempt ${IframeManager.iframeLoadAttempts}/${IframeManager.maxRetryAttempts + 1})`, this.getIframeContext());
545
- const iframe = document.createElement('iframe');
546
- let messageListener = null;
752
+ const waasSDKContainer = this.createWaasSDKContainer();
547
753
  const context = _extends({}, this.getIframeContext(), {
548
754
  attempt: IframeManager.iframeLoadAttempts
549
755
  });
550
- // Set up timeout that will trigger iframe error, a retry will be triggered on this iframe error
551
- const iframeTimeoutId = setTimeout(()=>{
552
- if (iframe.onerror) {
553
- iframe.onerror('Iframe load timeout');
756
+ const handleError = (error)=>{
757
+ clearTimeout(timeoutId);
758
+ unsubscribeHandshake();
759
+ unsubscribeError();
760
+ waasSDKContainer.destroy();
761
+ // Check if we should retry
762
+ if (IframeManager.iframeLoadAttempts <= IframeManager.maxRetryAttempts) {
763
+ const errorMsg = error instanceof Error ? error.message : String(error);
764
+ this.logger.warn(`(loadIframe) Iframe failed to load on attempt ${IframeManager.iframeLoadAttempts}, retrying... context: ${JSON.stringify(context)}, error: ${errorMsg}`);
765
+ // Retry after a short delay
766
+ setTimeout(attemptLoad, 1000);
767
+ } else {
768
+ // Max retries reached, give up
769
+ this.logger.error('Iframe failed to load after all retry attempts: ', error);
770
+ this.resetSharedWaasSDKContainer();
771
+ IframeManager.iframeLoadAttempts = 0;
772
+ reject(new Error(`Failed to load iframe after all retry attempts... context: ${JSON.stringify(context)}`));
554
773
  }
555
- }, IframeManager.iframeLoadTimeout);
556
- const resolveWithCleanup = ()=>{
557
- cleanupCspListener();
558
- resolve();
559
- };
560
- const rejectWithCleanup = (reason)=>{
561
- cleanupCspListener();
562
- reject(reason);
563
774
  };
564
- messageListener = this.createMessageListener(iframe, iframeTimeoutId, resolveWithCleanup);
565
- window.addEventListener('message', messageListener);
566
- this.configureIframe(iframe);
567
- this.setIframeSource(iframe);
568
- this.logger.debug('Creating iframe with src:', iframe.src);
569
- document.body.appendChild(iframe);
570
- this.setupIframeEventHandlersWithRetry(iframe, messageListener, iframeTimeoutId, rejectWithCleanup, attemptLoad, context);
775
+ // Set up timeout (handleError closure captures timeoutId/unsubscribe* declared below;
776
+ // they are assigned synchronously before any async callback can fire).
777
+ const timeoutId = setTimeout(()=>{
778
+ handleError('Iframe load timeout');
779
+ }, IframeManager.iframeLoadTimeout);
780
+ // Surface container-level errors (e.g. iframe.onerror) so we don't wait for the full timeout.
781
+ const unsubscribeError = waasSDKContainer.onError(handleError);
782
+ // Listen for the handshake message through the sandbox host's onMessage
783
+ const unsubscribeHandshake = waasSDKContainer.onMessage((data)=>{
784
+ if (data === `iframe-ready-${this.instanceId}`) {
785
+ clearTimeout(timeoutId);
786
+ unsubscribeHandshake();
787
+ unsubscribeError();
788
+ IframeManager.sharedWaasSDKContainer = waasSDKContainer;
789
+ this.waasSDKContainer = waasSDKContainer;
790
+ IframeManager.iframeInstanceCount++;
791
+ IframeManager.iframeLoadAttempts = 0; // Reset retry counter on success
792
+ resolve();
793
+ this.logger.debug('Iframe loaded successfully...', this.getIframeContext());
794
+ }
795
+ });
796
+ const url = this.buildIframeUrl();
797
+ this.logger.debug('Creating iframe with src:', url);
798
+ waasSDKContainer.setUrl(url).catch(handleError);
571
799
  };
572
800
  // Start the first attempt
573
801
  attemptLoad();
574
802
  });
575
803
  }
576
- setupIframeEventHandlersWithRetry(iframe, messageListener, iframeTimeoutId, reject, attemptLoad, context) {
577
- iframe.onload = ()=>{
578
- this.logger.debug('Iframe onload fired, waiting for ready message...');
579
- };
580
- iframe.onerror = (error)=>{
581
- if (messageListener) {
582
- window.removeEventListener('message', messageListener);
583
- }
584
- clearTimeout(iframeTimeoutId);
585
- // Check if we should retry
586
- if (IframeManager.iframeLoadAttempts <= IframeManager.maxRetryAttempts) {
587
- const errorMsg = error instanceof Error ? error.message : 'Unknown error occurred.';
588
- this.logger.warn(`(loadIframe) Iframe failed to load on attempt ${IframeManager.iframeLoadAttempts}, retrying... context: ${JSON.stringify(context)}, error: ${errorMsg}`);
589
- // Clean up current attempt
590
- if (iframe.parentNode) {
591
- iframe.parentNode.removeChild(iframe);
592
- }
593
- // Retry after a short delay
594
- setTimeout(()=>{
595
- attemptLoad();
596
- }, 1000); // 1 second delay between retries
597
- } else {
598
- // Max retries reached, give up
599
- this.logger.error('Iframe failed to load after all retry attempts: ', error);
600
- this.resetSharedIframe();
601
- IframeManager.iframeLoadAttempts = 0;
602
- reject(new Error(`Failed to load iframe after all retry attempts... context: ${JSON.stringify(context)}`));
603
- }
604
- };
605
- }
606
804
  getIframeContext() {
607
805
  var _this_sdkVersion;
608
806
  return {
@@ -614,87 +812,6 @@ class IframeManager {
614
812
  iframeLoadTimeout: IframeManager.iframeLoadTimeout
615
813
  };
616
814
  }
617
- createMessageListener(iframe, iframeTimeoutId, resolve) {
618
- const messageListener = (event)=>{
619
- if (event.source === iframe.contentWindow && event.data === `iframe-ready-${this.instanceId}`) {
620
- window.removeEventListener('message', messageListener);
621
- clearTimeout(iframeTimeoutId);
622
- IframeManager.sharedIframe = iframe;
623
- this.iframe = iframe;
624
- IframeManager.iframeInstanceCount++;
625
- IframeManager.iframeLoadAttempts = 0; // Reset retry counter on success
626
- resolve();
627
- this.logger.debug('Iframe loaded successfully...', this.getIframeContext());
628
- }
629
- };
630
- return messageListener;
631
- }
632
- configureIframe(iframe) {
633
- iframe.style.display = 'none';
634
- iframe.setAttribute('title', 'Dynamic Wallet Iframe');
635
- iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-downloads');
636
- iframe.setAttribute('referrerpolicy', 'origin');
637
- iframe.style.position = 'fixed';
638
- iframe.style.top = '0';
639
- iframe.style.left = '0';
640
- iframe.style.width = '0';
641
- iframe.style.height = '0';
642
- iframe.style.border = 'none';
643
- iframe.style.pointerEvents = 'none';
644
- }
645
- buildIframeUrlSearchParams() {
646
- var _this_instanceId, _this_sdkVersion, _this_baseClientKeysharesRelayApiUrl;
647
- const params = new URLSearchParams({
648
- instanceId: (_this_instanceId = this.instanceId) != null ? _this_instanceId : '',
649
- hostOrigin: window.location.origin,
650
- environmentId: this.environmentId,
651
- baseApiUrl: this.baseApiUrl,
652
- baseMPCRelayApiUrl: this.baseMPCRelayApiUrl,
653
- sdkVersion: (_this_sdkVersion = this.sdkVersion) != null ? _this_sdkVersion : '',
654
- debug: String(this.debug),
655
- baseClientKeysharesRelayApiUrl: (_this_baseClientKeysharesRelayApiUrl = this.baseClientKeysharesRelayApiUrl) != null ? _this_baseClientKeysharesRelayApiUrl : '',
656
- secureStorage: this.secureStorage ? 'true' : ''
657
- });
658
- const seenAdditional = new Set();
659
- for (const raw of this.additionalTrustedOrigins){
660
- const trimmed = raw.trim();
661
- if (!trimmed) {
662
- continue;
663
- }
664
- if (trimmed.length > IframeManager.maxTrustedOriginStringLength) {
665
- this.logger.warn('Skipping additionalTrustedOrigin (exceeds max length)');
666
- continue;
667
- }
668
- try {
669
- const u = new URL(trimmed);
670
- if (u.protocol !== 'http:' && u.protocol !== 'https:') {
671
- this.logger.warn('Skipping additionalTrustedOrigin (unsupported scheme)', {
672
- trimmed
673
- });
674
- continue;
675
- }
676
- const origin = u.origin;
677
- if (seenAdditional.has(origin)) {
678
- continue;
679
- }
680
- if (seenAdditional.size >= IframeManager.maxAdditionalTrustedOrigins) {
681
- this.logger.warn('additionalTrustedOrigin limit reached, extra entries ignored');
682
- break;
683
- }
684
- seenAdditional.add(origin);
685
- params.append(FRAME_ANCESTORS_QUERY_PARAM, origin);
686
- } catch (e) {
687
- this.logger.warn('Skipping invalid additionalTrustedOrigin', {
688
- trimmed
689
- });
690
- }
691
- }
692
- return params;
693
- }
694
- setIframeSource(iframe) {
695
- const params = this.buildIframeUrlSearchParams();
696
- iframe.src = `${this.iframeDomain}/waas-v1/${this.environmentId}?${params.toString()}`;
697
- }
698
815
  /**
699
816
  * Load an iframe for a specific container
700
817
  * @param {HTMLElement} container - The container to which the iframe will be attached
@@ -750,7 +867,6 @@ class IframeManager {
750
867
  window.removeEventListener('message', messageListener);
751
868
  }
752
869
  clearTimeout(iframeTimeoutId);
753
- IframeManager.sharedIframe = iframe;
754
870
  this.iframe = iframe;
755
871
  IframeManager.iframeInstanceCount++;
756
872
  resolve(iframe);
@@ -834,16 +950,16 @@ class IframeManager {
834
950
  error: error instanceof Error ? error.message : error
835
951
  });
836
952
  }
837
- if (this.iframe) {
953
+ if (this.waasSDKContainer) {
838
954
  IframeManager.iframeInstanceCount--;
839
- if (IframeManager.sharedIframe && IframeManager.iframeInstanceCount === 0) {
955
+ if (IframeManager.sharedWaasSDKContainer && IframeManager.iframeInstanceCount === 0) {
840
956
  this.cleanupBridge == null ? void 0 : this.cleanupBridge.call(this);
841
957
  this.cleanupBridge = null;
842
- document.body.removeChild(IframeManager.sharedIframe);
843
- IframeManager.sharedIframe = null;
958
+ IframeManager.sharedWaasSDKContainer.destroy();
959
+ IframeManager.sharedWaasSDKContainer = null;
844
960
  IframeManager.iframeLoadPromise = null;
845
961
  }
846
- this.iframe = null;
962
+ this.waasSDKContainer = null;
847
963
  }
848
964
  }
849
965
  constructor({ environmentId, baseApiUrl, baseMPCRelayApiUrl, chainName, sdkVersion, authMode = core.AuthMode.HEADER, authToken, debug, baseClientKeysharesRelayApiUrl, additionalTrustedOrigins }, internalOptions){
@@ -856,6 +972,7 @@ class IframeManager {
856
972
  * listeners. Tracked so we can rebuild the bridge on iframe recovery without
857
973
  * leaking listeners against the dead iframe. */ this.cleanupBridge = null;
858
974
  this.iframe = null;
975
+ this.waasSDKContainer = null;
859
976
  this.environmentId = environmentId;
860
977
  this.authToken = authToken;
861
978
  this.authMode = authMode;
@@ -875,6 +992,8 @@ class IframeManager {
875
992
  if (internalOptions == null ? void 0 : internalOptions.getSignedSessionId) {
876
993
  this.getSignedSessionIdCallback = internalOptions.getSignedSessionId;
877
994
  }
995
+ var _internalOptions_createWaasSDKContainer;
996
+ this.createWaasSDKContainer = (_internalOptions_createWaasSDKContainer = internalOptions == null ? void 0 : internalOptions.createWaasSDKContainer) != null ? _internalOptions_createWaasSDKContainer : createIframeWaasSDKContainer;
878
997
  const environment = core.getEnvironmentFromUrl(baseApiUrl);
879
998
  this.iframeDomain = core.IFRAME_DOMAIN_MAP[environment];
880
999
  if (this.authMode === core.AuthMode.COOKIE) {
@@ -890,7 +1009,7 @@ IframeManager.iframeLoadPromise = null;
890
1009
  IframeManager.iframeLoadTimeout = 10000;
891
1010
  IframeManager.iframeLoadAttempts = 0;
892
1011
  IframeManager.maxRetryAttempts = 1;
893
- IframeManager.sharedIframe = null;
1012
+ IframeManager.sharedWaasSDKContainer = null;
894
1013
  IframeManager.iframeInstanceCount = 0;
895
1014
  IframeManager.maxAdditionalTrustedOrigins = 32;
896
1015
  IframeManager.maxTrustedOriginStringLength = 200;
@@ -1321,3 +1440,4 @@ Object.defineProperty(exports, "WalletOperation", {
1321
1440
  get: function () { return core.WalletOperation; }
1322
1441
  });
1323
1442
  exports.DynamicWalletClient = DynamicWalletClient;
1443
+ exports.createIframeWaasSDKContainer = createIframeWaasSDKContainer;