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

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