@dynamic-labs/waas 4.88.6 → 4.90.0

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/CHANGELOG.md CHANGED
@@ -1,4 +1,38 @@
1
1
 
2
+ ## [4.90.0](https://github.com/dynamic-labs/dynamic-auth/compare/v4.89.0...v4.90.0) (2026-06-19)
3
+
4
+
5
+ ### Features
6
+
7
+ * add microsoft to social providers list ([#11631](https://github.com/dynamic-labs/dynamic-auth/issues/11631)) ([761b82d](https://github.com/dynamic-labs/dynamic-auth/commit/761b82dea2e2e86ec1c8b3fc8bab32ed72492132))
8
+ * **sdk-react-core:** preconnect to waas iframe origin at auth-flow start ([#11549](https://github.com/dynamic-labs/dynamic-auth/issues/11549)) ([1bb810c](https://github.com/dynamic-labs/dynamic-auth/commit/1bb810c4eb3d592b43f62c1eaf5450a0e42981c9)), closes [dynamic-labs/dynamic-wallet-sdk#1314](https://github.com/dynamic-labs/dynamic-wallet-sdk/issues/1314)
9
+ * **waas:** opt into SDK eager key-share recovery, drop host RECOVER loop (DYNT-1125) ([#11620](https://github.com/dynamic-labs/dynamic-auth/issues/11620)) ([4bef955](https://github.com/dynamic-labs/dynamic-auth/commit/4bef955b902a7e484ba8bd6323582843bb692a17))
10
+ * **wallet-connector-core:** add isMidnightConnector util + Midnight interface hooks ([#11005](https://github.com/dynamic-labs/dynamic-auth/issues/11005)) ([55117a8](https://github.com/dynamic-labs/dynamic-auth/commit/55117a8cf59640d00b358f68be8df6b29231fab5))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **sdk-react-core:** gate smart-wallet init on user:basic scope ([#11624](https://github.com/dynamic-labs/dynamic-auth/issues/11624)) ([9f891a4](https://github.com/dynamic-labs/dynamic-auth/commit/9f891a46a191532acdd4a91b74c4586089ef0e1a)), closes [#11623](https://github.com/dynamic-labs/dynamic-auth/issues/11623) [#11623](https://github.com/dynamic-labs/dynamic-auth/issues/11623) [#11626](https://github.com/dynamic-labs/dynamic-auth/issues/11626)
16
+ * **sdk-react-core:** search wallet list by metadata name in addition to connector name ([#11638](https://github.com/dynamic-labs/dynamic-auth/issues/11638)) ([a647614](https://github.com/dynamic-labs/dynamic-auth/commit/a647614bcba769e09ef59bf1ddb925ce9835842e))
17
+ * **waas:** rebuild WaaS client on session change after step-up reauth ([#11581](https://github.com/dynamic-labs/dynamic-auth/issues/11581)) ([2b7093e](https://github.com/dynamic-labs/dynamic-auth/commit/2b7093e27f57f1dfcdd303361d436d9bf33e28a9))
18
+ * **waas:** refuse to build WaaS client while auth scope is pending (createRooms 401 chokepoint) ([#11623](https://github.com/dynamic-labs/dynamic-auth/issues/11623)) ([08755f0](https://github.com/dynamic-labs/dynamic-auth/commit/08755f0bda2b74c7bcdff3d0ac9c228a5205ad6e)), closes [#11624](https://github.com/dynamic-labs/dynamic-auth/issues/11624)
19
+ * **wallet-connect:** skip address comparison on reconnect when expectedAddress is empty ([#11639](https://github.com/dynamic-labs/dynamic-auth/issues/11639)) ([8d665cf](https://github.com/dynamic-labs/dynamic-auth/commit/8d665cf3bf1fb0e498d71f14cc8a53d91593c23d))
20
+
21
+ ## [4.89.0](https://github.com/dynamic-labs/dynamic-auth/compare/v4.88.6...v4.89.0) (2026-06-16)
22
+
23
+
24
+ ### Features
25
+
26
+ * **moonpay:** add useMoonPayOnramp hook ([#11405](https://github.com/dynamic-labs/dynamic-auth/issues/11405)) ([48cb62f](https://github.com/dynamic-labs/dynamic-auth/commit/48cb62fefecb511fcba9359ee6f6096dfef0b125))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * get wallet metadata from wallet-book when its present ([#11566](https://github.com/dynamic-labs/dynamic-auth/issues/11566)) ([a1a8ad5](https://github.com/dynamic-labs/dynamic-auth/commit/a1a8ad53c063157b189aea138fa338fcc67dc4dd))
32
+ * **waas:** instant logout — clear key share host-side, background iframe cleanup ([#11583](https://github.com/dynamic-labs/dynamic-auth/issues/11583)) ([0ab3378](https://github.com/dynamic-labs/dynamic-auth/commit/0ab3378efdbbe233069b17fad62b0c126397bc3a))
33
+ * **wagmi-connector:** await disconnect before connect to defuse SyncDynamicWagmi race DYNT-549 ([#11579](https://github.com/dynamic-labs/dynamic-auth/issues/11579)) ([6ac7c0e](https://github.com/dynamic-labs/dynamic-auth/commit/6ac7c0e02ed863342047bc5a1e60be3a66b8a425)), closes [#11131](https://github.com/dynamic-labs/dynamic-auth/issues/11131) [#11513](https://github.com/dynamic-labs/dynamic-auth/issues/11513) [#11516](https://github.com/dynamic-labs/dynamic-auth/issues/11516) [#11496](https://github.com/dynamic-labs/dynamic-auth/issues/11496) [#11513](https://github.com/dynamic-labs/dynamic-auth/issues/11513) [/github.com/dynamic-labs/dynamic-auth/blob/main/packages/wagmi-connector/src/lib/hooks/useConnectorId/useConnectorId.ts#L19-L26](https://github.com/dynamic-labs//github.com/dynamic-labs/dynamic-auth/blob/main/packages/wagmi-connector/src/lib/hooks/useConnectorId/useConnectorId.ts/issues/L19-L26) [#11131](https://github.com/dynamic-labs/dynamic-auth/issues/11131) [#11516](https://github.com/dynamic-labs/dynamic-auth/issues/11516) [#11513](https://github.com/dynamic-labs/dynamic-auth/issues/11513)
34
+ * **react-native:** resolve intermittent "Wallet with id <uuid> not found" after login ([#11575](https://github.com/dynamic-labs/dynamic-auth/issues/11575)) ([9d7a246](https://github.com/dynamic-labs/dynamic-auth/commit/9d7a246167927b9339dc33c10977c6d12783ab88))
35
+
2
36
  ### [4.88.6](https://github.com/dynamic-labs/dynamic-auth/compare/v4.88.5...v4.88.6) (2026-06-13)
3
37
 
4
38
 
package/package.cjs CHANGED
@@ -3,6 +3,6 @@
3
3
 
4
4
  Object.defineProperty(exports, '__esModule', { value: true });
5
5
 
6
- var version = "4.88.6";
6
+ var version = "4.90.0";
7
7
 
8
8
  exports.version = version;
package/package.js CHANGED
@@ -1,4 +1,4 @@
1
1
  'use client'
2
- var version = "4.88.6";
2
+ var version = "4.90.0";
3
3
 
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dynamic-labs/waas",
3
- "version": "4.88.6",
3
+ "version": "4.90.0",
4
4
  "type": "module",
5
5
  "author": "Dynamic Labs, Inc.",
6
6
  "license": "MIT",
@@ -16,18 +16,18 @@
16
16
  "./package.json": "./package.json"
17
17
  },
18
18
  "dependencies": {
19
- "@dynamic-labs-sdk/client": "1.8.2",
20
- "@dynamic-labs/assert-package-version": "4.88.6",
21
- "@dynamic-labs/sdk-api-core": "0.0.1015",
22
- "@dynamic-labs-wallet/browser-wallet-client": "1.0.16",
23
- "@dynamic-labs-wallet/forward-mpc-client": "0.10.1",
24
- "@dynamic-labs/ethereum-core": "4.88.6",
25
- "@dynamic-labs/logger": "4.88.6",
26
- "@dynamic-labs/solana-core": "4.88.6",
27
- "@dynamic-labs/sui-core": "4.88.6",
28
- "@dynamic-labs/utils": "4.88.6",
29
- "@dynamic-labs/wallet-book": "4.88.6",
30
- "@dynamic-labs/wallet-connector-core": "4.88.6"
19
+ "@dynamic-labs-sdk/client": "1.12.1",
20
+ "@dynamic-labs/assert-package-version": "4.90.0",
21
+ "@dynamic-labs/sdk-api-core": "0.0.1046",
22
+ "@dynamic-labs-wallet/browser-wallet-client": "1.0.42",
23
+ "@dynamic-labs-wallet/forward-mpc-client": "0.12.0",
24
+ "@dynamic-labs/ethereum-core": "4.90.0",
25
+ "@dynamic-labs/logger": "4.90.0",
26
+ "@dynamic-labs/solana-core": "4.90.0",
27
+ "@dynamic-labs/sui-core": "4.90.0",
28
+ "@dynamic-labs/utils": "4.90.0",
29
+ "@dynamic-labs/wallet-book": "4.90.0",
30
+ "@dynamic-labs/wallet-connector-core": "4.90.0"
31
31
  },
32
32
  "peerDependencies": {}
33
33
  }
@@ -11,7 +11,11 @@ var _package = require('../package.cjs');
11
11
  var constants = require('../utils/constants.cjs');
12
12
  var createWaasClientSecureStorage = require('../utils/createWaasClientSecureStorage.cjs');
13
13
  var instrumentation = require('../utils/instrumentation.cjs');
14
+ var tokenHasPendingAuthScope = require('../utils/tokenHasPendingAuthScope.cjs');
14
15
 
16
+ // Fallback error code used when a thrown value carries no code, or when
17
+ // formatting the error itself fails.
18
+ const UNKNOWN_ERROR_CODE = 'unknown';
15
19
  // This class is common across all waas connectors
16
20
  class WaasExportHandler {
17
21
  constructor() {
@@ -32,9 +36,6 @@ const withDynamicWaas = (BaseClass) => {
32
36
  setGetAuthTokenFunction(getAuthToken) {
33
37
  this.getAuthToken = getAuthToken;
34
38
  }
35
- setOnUnauthorizedFunction(onUnauthorized) {
36
- this.onUnauthorized = onUnauthorized;
37
- }
38
39
  setWaasAuthMode(authMode) {
39
40
  this.authMode = authMode;
40
41
  }
@@ -191,14 +192,15 @@ const withDynamicWaas = (BaseClass) => {
191
192
  baseClientKeysharesRelayApiUrl: this.baseClientKeysharesRelayApiUrl,
192
193
  baseMPCRelayApiUrl: this.relayUrl || constants.DEFAULT_BASE_MPC_RELAY_API_URL,
193
194
  chainName: this.chainName,
195
+ // Opt into the SDK self-driving eager key-share recovery on auth-token
196
+ // arrival, so the host no longer runs its own per-wallet RECOVER loop.
197
+ eagerlyRecoverKeyShares: true,
194
198
  environmentId: this.environmentId,
195
199
  sdkVersion: _package.version,
196
- }, Object.assign(Object.assign(Object.assign({}, (utils.PlatformService.isWaasSecureStorageSupported
200
+ }, Object.assign(Object.assign({}, (utils.PlatformService.isWaasSecureStorageSupported
197
201
  ? { secureStorage: createWaasClientSecureStorage.createWaasClientSecureStorage() }
198
202
  : {})), (this.getSignedSessionId
199
203
  ? { getSignedSessionId: this.getSignedSessionId }
200
- : {})), (this.onUnauthorized
201
- ? { onUnauthorized: this.onUnauthorized }
202
204
  : {})));
203
205
  this.instrumentAsync({
204
206
  context: traceContext,
@@ -209,9 +211,29 @@ const withDynamicWaas = (BaseClass) => {
209
211
  return client;
210
212
  });
211
213
  }
212
- getWaasWalletClient(traceContext) {
213
- return _tslib.__awaiter(this, void 0, void 0, function* () {
214
+ getWaasWalletClient(traceContext_1) {
215
+ return _tslib.__awaiter(this, arguments, void 0, function* (traceContext, { forceRebuild = false } = {}) {
216
+ var _a;
217
+ // forceRebuild drops the cached client so the iframe re-auths with the
218
+ // fresh session (e.g. after a step-up reauth) instead of 401-ing on the
219
+ // stale cached token. Callers must only force a rebuild when a token is
220
+ // available (see getWaasWalletConnector's live-token guard); otherwise
221
+ // createDynamicWaasClient would throw "Auth token is required".
222
+ if (forceRebuild) {
223
+ this.dynamicWaasClient = undefined;
224
+ }
214
225
  if (!this.dynamicWaasClient) {
226
+ // Chokepoint: never build + initialize the iframe client while the auth
227
+ // token is still pending MFA/KYC/device registration. The iframe's
228
+ // init-time room-cache pre-warm fires `createRooms` using the token
229
+ // captured at construction — before any per-operation token resync
230
+ // exists to correct it. Built during the pending window, that pre-warm
231
+ // 401s and trips the iframe's unauthorized → forced-logout path. Every
232
+ // WaaS caller funnels through here, so this single guard covers auto
233
+ // wallet creation, ZeroDev smart-wallet init, getWallet, etc.
234
+ if (tokenHasPendingAuthScope.tokenHasPendingAuthScope((_a = this.getAuthToken) === null || _a === void 0 ? void 0 : _a.call(this))) {
235
+ throw new utils.WaasAuthScopePendingError();
236
+ }
215
237
  this.dynamicWaasClient = yield this.createDynamicWaasClient(traceContext);
216
238
  }
217
239
  return this.dynamicWaasClient;
@@ -609,9 +631,18 @@ const withDynamicWaas = (BaseClass) => {
609
631
  }
610
632
  endSession(reason) {
611
633
  return _tslib.__awaiter(this, void 0, void 0, function* () {
612
- const waasClient = yield this.getWaasWalletClient();
613
- if (!waasClient) {
614
- return;
634
+ // Building the wallet client requires an auth token (createDynamicWaasClient
635
+ // throws in header auth mode when none is present); the new scope guard in
636
+ // getWaasWalletClient also throws during pending-auth (MFA/KYC/device). On
637
+ // logout we must not let either throw abort logout — the security-critical
638
+ // key share removal below does not need the client. Only the iframe cleanup
639
+ // does, so we proceed without it if the client can't be obtained.
640
+ let waasClient;
641
+ try {
642
+ waasClient = yield this.getWaasWalletClient();
643
+ }
644
+ catch (error) {
645
+ this.instrument('[endSession] getWaasWalletClient failed; proceeding', Object.assign({ key: 'endSession-getClient-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
615
646
  }
616
647
  // When a session token expires, we preserve key shares in storage instead
617
648
  // of clearing them. Customers with short-lived sessions (minutes) were
@@ -624,7 +655,32 @@ const withDynamicWaas = (BaseClass) => {
624
655
  // in. On explicit user-initiated logout, shares are always cleared for
625
656
  // security.
626
657
  if (reason !== 'token-expired') {
627
- yield waasClient.cleanup();
658
+ // Clear the MPC key share directly on the host's local secure storage.
659
+ // This is fast and guaranteed, independent of the iframe round-trip
660
+ // cleanup below — whose request-channel timeout (plus a recovery retry)
661
+ // can block logout for ~10s on slow devices where secureStorage is slow
662
+ // to ACK. Awaited because it is a local operation (milliseconds).
663
+ const accountAddress = yield this.getActiveAccountAddress();
664
+ if (accountAddress) {
665
+ try {
666
+ yield createWaasClientSecureStorage.createWaasClientSecureStorage().removeClientKeyShare(accountAddress);
667
+ }
668
+ catch (error) {
669
+ this.instrument('[endSession] direct key share removal failed', Object.assign({ key: 'endSession-removeClientKeyShare-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
670
+ }
671
+ }
672
+ // Tear down the iframe and run its best-effort storage cleanup in the
673
+ // background so logout returns immediately instead of blocking on the
674
+ // iframe cleanup's request-channel timeout. The iframe handler already
675
+ // swallows its own errors, and the DOM teardown obsoletes anything it
676
+ // misses. Promise.resolve guards against a non-promise return in tests.
677
+ // Skipped when the client could not be built/obtained (no token, or
678
+ // pending-auth scope guard) — there is no iframe to tear down then.
679
+ if (waasClient) {
680
+ void Promise.resolve(waasClient.cleanup()).catch((error) => {
681
+ this.instrument('[endSession] background iframe cleanup failed', Object.assign({ key: 'endSession-cleanup-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
682
+ });
683
+ }
628
684
  }
629
685
  this.dynamicWaasClient = undefined;
630
686
  });
@@ -661,6 +717,33 @@ const withDynamicWaas = (BaseClass) => {
661
717
  .map((byte) => byte.toString(16).padStart(2, '0'))
662
718
  .join('');
663
719
  }
720
+ /**
721
+ * Normalizes an unknown thrown value into the error fields used across
722
+ * instrumentation so callers don't re-derive them at each catch site.
723
+ *
724
+ * Wrapped in try/catch: this is best-effort logging, and a value that
725
+ * resists formatting (e.g. an object whose toString throws) must never
726
+ * turn a log line into an exception that blocks the calling flow such as
727
+ * logout.
728
+ *
729
+ * `public` (not `private`) because the TS `private` keyword can't be used
730
+ * on this mixin's exported class expression (TS4094), and ECMAScript
731
+ * `#private` isn't supported by the package's Jest/Babel transform. It is
732
+ * internal-only despite the modifier — same constraint as `instrument`.
733
+ */
734
+ buildErrorInstrumentContext(error) {
735
+ var _a, _b;
736
+ try {
737
+ return {
738
+ errorCode: (_a = error === null || error === void 0 ? void 0 : error.code) !== null && _a !== void 0 ? _a : UNKNOWN_ERROR_CODE,
739
+ errorMessage: error instanceof Error ? error.message : String(error),
740
+ errorType: (_b = error === null || error === void 0 ? void 0 : error.constructor) === null || _b === void 0 ? void 0 : _b.name,
741
+ };
742
+ }
743
+ catch (_c) {
744
+ return { errorCode: UNKNOWN_ERROR_CODE };
745
+ }
746
+ }
664
747
  /**
665
748
  * Helper method to instrument with automatic properties inclusion
666
749
  */
@@ -676,7 +759,6 @@ const withDynamicWaas = (BaseClass) => {
676
759
  }
677
760
  instrumentAsync(_a) {
678
761
  return _tslib.__awaiter(this, arguments, void 0, function* ({ operation, resource, fn, context, }) {
679
- var _b, _c;
680
762
  const timing = new instrumentation.InstrumentationTimer(context === null || context === void 0 ? void 0 : context.startTime);
681
763
  if (context === null || context === void 0 ? void 0 : context.stepStartTime) {
682
764
  timing.setStepStartTime(context.stepStartTime);
@@ -693,11 +775,7 @@ const withDynamicWaas = (BaseClass) => {
693
775
  catch (error) {
694
776
  const isUserRejection = error instanceof utils.UserRejectedRequestError ||
695
777
  (error === null || error === void 0 ? void 0 : error.code) === 'user_rejected_request';
696
- const errorContext = {
697
- errorCode: (_b = error === null || error === void 0 ? void 0 : error.code) !== null && _b !== void 0 ? _b : 'unknown',
698
- errorMessage: error instanceof Error ? error.message : String(error),
699
- errorType: (_c = error === null || error === void 0 ? void 0 : error.constructor) === null || _c === void 0 ? void 0 : _c.name,
700
- };
778
+ const errorContext = this.buildErrorInstrumentContext(error);
701
779
  if (isUserRejection) {
702
780
  // User-initiated cancellations are expected — debug only, no backend noise
703
781
  this.logger.debug(`[${operation}] ${resource} - cancelled`, Object.assign(Object.assign({ key: `${resource}-cancelled`, operation, stepTime: timing.getStepElapsed(), time: timing.getElapsed() }, context), errorContext));
@@ -23,7 +23,6 @@ export declare const withDynamicWaas: <T extends abstract new (...args: any[]) =
23
23
  getElevatedAccessToken?: ((props: {
24
24
  scope: TokenScope;
25
25
  }) => Promise<string | undefined>) | undefined;
26
- onUnauthorized?: (() => void | Promise<void>) | undefined;
27
26
  environmentId?: string | undefined;
28
27
  baseApiUrl?: string | undefined;
29
28
  relayUrl?: string | undefined;
@@ -35,7 +34,6 @@ export declare const withDynamicWaas: <T extends abstract new (...args: any[]) =
35
34
  __exportHandler: WaasExportHandler;
36
35
  validateActiveWallet(expectedAddress: string): Promise<void>;
37
36
  setGetAuthTokenFunction(getAuthToken: () => string): void;
38
- setOnUnauthorizedFunction(onUnauthorized: () => void | Promise<void>): void;
39
37
  setWaasAuthMode(authMode: 'cookie' | 'header'): void;
40
38
  setGetMfaTokenFunction(getMfaToken: (props?: {
41
39
  mfaAction?: MFAAction;
@@ -76,7 +74,9 @@ export declare const withDynamicWaas: <T extends abstract new (...args: any[]) =
76
74
  password?: string;
77
75
  }): Promise<void>;
78
76
  createDynamicWaasClient(traceContext?: TraceContext): Promise<DynamicWalletClient>;
79
- getWaasWalletClient(traceContext?: TraceContext): Promise<DynamicWalletClient>;
77
+ getWaasWalletClient(traceContext?: TraceContext, { forceRebuild }?: {
78
+ forceRebuild?: boolean;
79
+ }): Promise<DynamicWalletClient>;
80
80
  createWalletAccount({ thresholdSignatureScheme, password, bitcoinConfig, }?: {
81
81
  thresholdSignatureScheme?: string;
82
82
  password?: string;
@@ -166,6 +166,29 @@ export declare const withDynamicWaas: <T extends abstract new (...args: any[]) =
166
166
  */
167
167
  getConnectedAccounts(): Promise<string[]>;
168
168
  generateTraceId(): string;
169
+ /**
170
+ * Normalizes an unknown thrown value into the error fields used across
171
+ * instrumentation so callers don't re-derive them at each catch site.
172
+ *
173
+ * Wrapped in try/catch: this is best-effort logging, and a value that
174
+ * resists formatting (e.g. an object whose toString throws) must never
175
+ * turn a log line into an exception that blocks the calling flow such as
176
+ * logout.
177
+ *
178
+ * `public` (not `private`) because the TS `private` keyword can't be used
179
+ * on this mixin's exported class expression (TS4094), and ECMAScript
180
+ * `#private` isn't supported by the package's Jest/Babel transform. It is
181
+ * internal-only despite the modifier — same constraint as `instrument`.
182
+ */
183
+ buildErrorInstrumentContext(error: unknown): {
184
+ errorCode: string | import("@dynamic-labs/utils").ErrorCode;
185
+ errorMessage: string;
186
+ errorType: string | undefined;
187
+ } | {
188
+ errorCode: string;
189
+ errorMessage?: undefined;
190
+ errorType?: undefined;
191
+ };
169
192
  /**
170
193
  * Helper method to instrument with automatic properties inclusion
171
194
  */
@@ -2,12 +2,16 @@
2
2
  import { __awaiter } from '../_virtual/_tslib.js';
3
3
  import { DynamicWalletClient } from '@dynamic-labs-wallet/browser-wallet-client';
4
4
  import { MFAAction, TokenScope } from '@dynamic-labs/sdk-api-core';
5
- import { DynamicError, UserRejectedRequestError, PlatformService } from '@dynamic-labs/utils';
5
+ import { DynamicError, UserRejectedRequestError, PlatformService, WaasAuthScopePendingError } from '@dynamic-labs/utils';
6
6
  import { version } from '../package.js';
7
7
  import { DEFAULT_BASE_API_URL, DEFAULT_BASE_MPC_RELAY_API_URL } from '../utils/constants.js';
8
8
  import { createWaasClientSecureStorage } from '../utils/createWaasClientSecureStorage.js';
9
9
  import { InstrumentationTimer } from '../utils/instrumentation.js';
10
+ import { tokenHasPendingAuthScope } from '../utils/tokenHasPendingAuthScope.js';
10
11
 
12
+ // Fallback error code used when a thrown value carries no code, or when
13
+ // formatting the error itself fails.
14
+ const UNKNOWN_ERROR_CODE = 'unknown';
11
15
  // This class is common across all waas connectors
12
16
  class WaasExportHandler {
13
17
  constructor() {
@@ -28,9 +32,6 @@ const withDynamicWaas = (BaseClass) => {
28
32
  setGetAuthTokenFunction(getAuthToken) {
29
33
  this.getAuthToken = getAuthToken;
30
34
  }
31
- setOnUnauthorizedFunction(onUnauthorized) {
32
- this.onUnauthorized = onUnauthorized;
33
- }
34
35
  setWaasAuthMode(authMode) {
35
36
  this.authMode = authMode;
36
37
  }
@@ -187,14 +188,15 @@ const withDynamicWaas = (BaseClass) => {
187
188
  baseClientKeysharesRelayApiUrl: this.baseClientKeysharesRelayApiUrl,
188
189
  baseMPCRelayApiUrl: this.relayUrl || DEFAULT_BASE_MPC_RELAY_API_URL,
189
190
  chainName: this.chainName,
191
+ // Opt into the SDK self-driving eager key-share recovery on auth-token
192
+ // arrival, so the host no longer runs its own per-wallet RECOVER loop.
193
+ eagerlyRecoverKeyShares: true,
190
194
  environmentId: this.environmentId,
191
195
  sdkVersion: version,
192
- }, Object.assign(Object.assign(Object.assign({}, (PlatformService.isWaasSecureStorageSupported
196
+ }, Object.assign(Object.assign({}, (PlatformService.isWaasSecureStorageSupported
193
197
  ? { secureStorage: createWaasClientSecureStorage() }
194
198
  : {})), (this.getSignedSessionId
195
199
  ? { getSignedSessionId: this.getSignedSessionId }
196
- : {})), (this.onUnauthorized
197
- ? { onUnauthorized: this.onUnauthorized }
198
200
  : {})));
199
201
  this.instrumentAsync({
200
202
  context: traceContext,
@@ -205,9 +207,29 @@ const withDynamicWaas = (BaseClass) => {
205
207
  return client;
206
208
  });
207
209
  }
208
- getWaasWalletClient(traceContext) {
209
- return __awaiter(this, void 0, void 0, function* () {
210
+ getWaasWalletClient(traceContext_1) {
211
+ return __awaiter(this, arguments, void 0, function* (traceContext, { forceRebuild = false } = {}) {
212
+ var _a;
213
+ // forceRebuild drops the cached client so the iframe re-auths with the
214
+ // fresh session (e.g. after a step-up reauth) instead of 401-ing on the
215
+ // stale cached token. Callers must only force a rebuild when a token is
216
+ // available (see getWaasWalletConnector's live-token guard); otherwise
217
+ // createDynamicWaasClient would throw "Auth token is required".
218
+ if (forceRebuild) {
219
+ this.dynamicWaasClient = undefined;
220
+ }
210
221
  if (!this.dynamicWaasClient) {
222
+ // Chokepoint: never build + initialize the iframe client while the auth
223
+ // token is still pending MFA/KYC/device registration. The iframe's
224
+ // init-time room-cache pre-warm fires `createRooms` using the token
225
+ // captured at construction — before any per-operation token resync
226
+ // exists to correct it. Built during the pending window, that pre-warm
227
+ // 401s and trips the iframe's unauthorized → forced-logout path. Every
228
+ // WaaS caller funnels through here, so this single guard covers auto
229
+ // wallet creation, ZeroDev smart-wallet init, getWallet, etc.
230
+ if (tokenHasPendingAuthScope((_a = this.getAuthToken) === null || _a === void 0 ? void 0 : _a.call(this))) {
231
+ throw new WaasAuthScopePendingError();
232
+ }
211
233
  this.dynamicWaasClient = yield this.createDynamicWaasClient(traceContext);
212
234
  }
213
235
  return this.dynamicWaasClient;
@@ -605,9 +627,18 @@ const withDynamicWaas = (BaseClass) => {
605
627
  }
606
628
  endSession(reason) {
607
629
  return __awaiter(this, void 0, void 0, function* () {
608
- const waasClient = yield this.getWaasWalletClient();
609
- if (!waasClient) {
610
- return;
630
+ // Building the wallet client requires an auth token (createDynamicWaasClient
631
+ // throws in header auth mode when none is present); the new scope guard in
632
+ // getWaasWalletClient also throws during pending-auth (MFA/KYC/device). On
633
+ // logout we must not let either throw abort logout — the security-critical
634
+ // key share removal below does not need the client. Only the iframe cleanup
635
+ // does, so we proceed without it if the client can't be obtained.
636
+ let waasClient;
637
+ try {
638
+ waasClient = yield this.getWaasWalletClient();
639
+ }
640
+ catch (error) {
641
+ this.instrument('[endSession] getWaasWalletClient failed; proceeding', Object.assign({ key: 'endSession-getClient-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
611
642
  }
612
643
  // When a session token expires, we preserve key shares in storage instead
613
644
  // of clearing them. Customers with short-lived sessions (minutes) were
@@ -620,7 +651,32 @@ const withDynamicWaas = (BaseClass) => {
620
651
  // in. On explicit user-initiated logout, shares are always cleared for
621
652
  // security.
622
653
  if (reason !== 'token-expired') {
623
- yield waasClient.cleanup();
654
+ // Clear the MPC key share directly on the host's local secure storage.
655
+ // This is fast and guaranteed, independent of the iframe round-trip
656
+ // cleanup below — whose request-channel timeout (plus a recovery retry)
657
+ // can block logout for ~10s on slow devices where secureStorage is slow
658
+ // to ACK. Awaited because it is a local operation (milliseconds).
659
+ const accountAddress = yield this.getActiveAccountAddress();
660
+ if (accountAddress) {
661
+ try {
662
+ yield createWaasClientSecureStorage().removeClientKeyShare(accountAddress);
663
+ }
664
+ catch (error) {
665
+ this.instrument('[endSession] direct key share removal failed', Object.assign({ key: 'endSession-removeClientKeyShare-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
666
+ }
667
+ }
668
+ // Tear down the iframe and run its best-effort storage cleanup in the
669
+ // background so logout returns immediately instead of blocking on the
670
+ // iframe cleanup's request-channel timeout. The iframe handler already
671
+ // swallows its own errors, and the DOM teardown obsoletes anything it
672
+ // misses. Promise.resolve guards against a non-promise return in tests.
673
+ // Skipped when the client could not be built/obtained (no token, or
674
+ // pending-auth scope guard) — there is no iframe to tear down then.
675
+ if (waasClient) {
676
+ void Promise.resolve(waasClient.cleanup()).catch((error) => {
677
+ this.instrument('[endSession] background iframe cleanup failed', Object.assign({ key: 'endSession-cleanup-failed', time: 0 }, this.buildErrorInstrumentContext(error)));
678
+ });
679
+ }
624
680
  }
625
681
  this.dynamicWaasClient = undefined;
626
682
  });
@@ -657,6 +713,33 @@ const withDynamicWaas = (BaseClass) => {
657
713
  .map((byte) => byte.toString(16).padStart(2, '0'))
658
714
  .join('');
659
715
  }
716
+ /**
717
+ * Normalizes an unknown thrown value into the error fields used across
718
+ * instrumentation so callers don't re-derive them at each catch site.
719
+ *
720
+ * Wrapped in try/catch: this is best-effort logging, and a value that
721
+ * resists formatting (e.g. an object whose toString throws) must never
722
+ * turn a log line into an exception that blocks the calling flow such as
723
+ * logout.
724
+ *
725
+ * `public` (not `private`) because the TS `private` keyword can't be used
726
+ * on this mixin's exported class expression (TS4094), and ECMAScript
727
+ * `#private` isn't supported by the package's Jest/Babel transform. It is
728
+ * internal-only despite the modifier — same constraint as `instrument`.
729
+ */
730
+ buildErrorInstrumentContext(error) {
731
+ var _a, _b;
732
+ try {
733
+ return {
734
+ errorCode: (_a = error === null || error === void 0 ? void 0 : error.code) !== null && _a !== void 0 ? _a : UNKNOWN_ERROR_CODE,
735
+ errorMessage: error instanceof Error ? error.message : String(error),
736
+ errorType: (_b = error === null || error === void 0 ? void 0 : error.constructor) === null || _b === void 0 ? void 0 : _b.name,
737
+ };
738
+ }
739
+ catch (_c) {
740
+ return { errorCode: UNKNOWN_ERROR_CODE };
741
+ }
742
+ }
660
743
  /**
661
744
  * Helper method to instrument with automatic properties inclusion
662
745
  */
@@ -672,7 +755,6 @@ const withDynamicWaas = (BaseClass) => {
672
755
  }
673
756
  instrumentAsync(_a) {
674
757
  return __awaiter(this, arguments, void 0, function* ({ operation, resource, fn, context, }) {
675
- var _b, _c;
676
758
  const timing = new InstrumentationTimer(context === null || context === void 0 ? void 0 : context.startTime);
677
759
  if (context === null || context === void 0 ? void 0 : context.stepStartTime) {
678
760
  timing.setStepStartTime(context.stepStartTime);
@@ -689,11 +771,7 @@ const withDynamicWaas = (BaseClass) => {
689
771
  catch (error) {
690
772
  const isUserRejection = error instanceof UserRejectedRequestError ||
691
773
  (error === null || error === void 0 ? void 0 : error.code) === 'user_rejected_request';
692
- const errorContext = {
693
- errorCode: (_b = error === null || error === void 0 ? void 0 : error.code) !== null && _b !== void 0 ? _b : 'unknown',
694
- errorMessage: error instanceof Error ? error.message : String(error),
695
- errorType: (_c = error === null || error === void 0 ? void 0 : error.constructor) === null || _c === void 0 ? void 0 : _c.name,
696
- };
774
+ const errorContext = this.buildErrorInstrumentContext(error);
697
775
  if (isUserRejection) {
698
776
  // User-initiated cancellations are expected — debug only, no backend noise
699
777
  this.logger.debug(`[${operation}] ${resource} - cancelled`, Object.assign(Object.assign({ key: `${resource}-cancelled`, operation, stepTime: timing.getStepElapsed(), time: timing.getElapsed() }, context), errorContext));
@@ -0,0 +1,55 @@
1
+ 'use client'
2
+ 'use strict';
3
+
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
6
+ var sdkApiCore = require('@dynamic-labs/sdk-api-core');
7
+
8
+ // Scopes that mean the user has not finished authenticating (MFA / KYC /
9
+ // device registration still pending). A JWT carrying any of these has not
10
+ // upgraded to `user:basic`, and the WaaS backend rejects calls made with it
11
+ // (e.g. createRooms) with a 401.
12
+ //
13
+ // Note `user:basic` and `waasBackupToken` are intentionally NOT in this list:
14
+ // backup / recovery flows legitimately run with a `waasBackupToken` scope, so
15
+ // only the pending-auth scopes below should block WaaS client creation.
16
+ const PENDING_AUTH_SCOPES = [
17
+ sdkApiCore.JwtScope.RequiresAdditionalAuth,
18
+ sdkApiCore.JwtScope.UserDataForm,
19
+ sdkApiCore.JwtScope.Deviceregister,
20
+ ];
21
+ const decodeScope = (authToken) => {
22
+ const payload = authToken === null || authToken === void 0 ? void 0 : authToken.split('.')[1];
23
+ if (!payload) {
24
+ return undefined;
25
+ }
26
+ try {
27
+ return JSON.parse(atob(payload)).scope;
28
+ }
29
+ catch (_a) {
30
+ return undefined;
31
+ }
32
+ };
33
+ /**
34
+ * Returns true when the auth token still carries a pending-authentication
35
+ * scope (`requiresAdditionalAuth` / `userDataForm` / `device:register`).
36
+ *
37
+ * The WaaS iframe's API client is a shared, mutable axios instance. The token
38
+ * captured at client construction is what the init-time room-cache pre-warm
39
+ * `createRooms` uses — it runs before any per-operation token resync. So if the
40
+ * client is built during this pending window, that pre-warm fires with the
41
+ * stale pre-`user:basic` token and 401s; the iframe reports the 401 back to the
42
+ * host, which force-logs-out the user. Callers must therefore refuse to build
43
+ * the client until the token clears (the vulnerable window is construction +
44
+ * init, not the whole client lifetime).
45
+ *
46
+ * A token with no decodable scope is treated as not-pending (returns false):
47
+ * the backend remains the source of truth and will reject anything invalid.
48
+ */
49
+ const tokenHasPendingAuthScope = (authToken) => {
50
+ var _a, _b;
51
+ const scopes = (_b = (_a = decodeScope(authToken)) === null || _a === void 0 ? void 0 : _a.split(' ')) !== null && _b !== void 0 ? _b : [];
52
+ return scopes.some((scope) => PENDING_AUTH_SCOPES.includes(scope));
53
+ };
54
+
55
+ exports.tokenHasPendingAuthScope = tokenHasPendingAuthScope;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Returns true when the auth token still carries a pending-authentication
3
+ * scope (`requiresAdditionalAuth` / `userDataForm` / `device:register`).
4
+ *
5
+ * The WaaS iframe's API client is a shared, mutable axios instance. The token
6
+ * captured at client construction is what the init-time room-cache pre-warm
7
+ * `createRooms` uses — it runs before any per-operation token resync. So if the
8
+ * client is built during this pending window, that pre-warm fires with the
9
+ * stale pre-`user:basic` token and 401s; the iframe reports the 401 back to the
10
+ * host, which force-logs-out the user. Callers must therefore refuse to build
11
+ * the client until the token clears (the vulnerable window is construction +
12
+ * init, not the whole client lifetime).
13
+ *
14
+ * A token with no decodable scope is treated as not-pending (returns false):
15
+ * the backend remains the source of truth and will reject anything invalid.
16
+ */
17
+ export declare const tokenHasPendingAuthScope: (authToken?: string) => boolean;
@@ -0,0 +1,51 @@
1
+ 'use client'
2
+ import { JwtScope } from '@dynamic-labs/sdk-api-core';
3
+
4
+ // Scopes that mean the user has not finished authenticating (MFA / KYC /
5
+ // device registration still pending). A JWT carrying any of these has not
6
+ // upgraded to `user:basic`, and the WaaS backend rejects calls made with it
7
+ // (e.g. createRooms) with a 401.
8
+ //
9
+ // Note `user:basic` and `waasBackupToken` are intentionally NOT in this list:
10
+ // backup / recovery flows legitimately run with a `waasBackupToken` scope, so
11
+ // only the pending-auth scopes below should block WaaS client creation.
12
+ const PENDING_AUTH_SCOPES = [
13
+ JwtScope.RequiresAdditionalAuth,
14
+ JwtScope.UserDataForm,
15
+ JwtScope.Deviceregister,
16
+ ];
17
+ const decodeScope = (authToken) => {
18
+ const payload = authToken === null || authToken === void 0 ? void 0 : authToken.split('.')[1];
19
+ if (!payload) {
20
+ return undefined;
21
+ }
22
+ try {
23
+ return JSON.parse(atob(payload)).scope;
24
+ }
25
+ catch (_a) {
26
+ return undefined;
27
+ }
28
+ };
29
+ /**
30
+ * Returns true when the auth token still carries a pending-authentication
31
+ * scope (`requiresAdditionalAuth` / `userDataForm` / `device:register`).
32
+ *
33
+ * The WaaS iframe's API client is a shared, mutable axios instance. The token
34
+ * captured at client construction is what the init-time room-cache pre-warm
35
+ * `createRooms` uses — it runs before any per-operation token resync. So if the
36
+ * client is built during this pending window, that pre-warm fires with the
37
+ * stale pre-`user:basic` token and 401s; the iframe reports the 401 back to the
38
+ * host, which force-logs-out the user. Callers must therefore refuse to build
39
+ * the client until the token clears (the vulnerable window is construction +
40
+ * init, not the whole client lifetime).
41
+ *
42
+ * A token with no decodable scope is treated as not-pending (returns false):
43
+ * the backend remains the source of truth and will reject anything invalid.
44
+ */
45
+ const tokenHasPendingAuthScope = (authToken) => {
46
+ var _a, _b;
47
+ const scopes = (_b = (_a = decodeScope(authToken)) === null || _a === void 0 ? void 0 : _a.split(' ')) !== null && _b !== void 0 ? _b : [];
48
+ return scopes.some((scope) => PENDING_AUTH_SCOPES.includes(scope));
49
+ };
50
+
51
+ export { tokenHasPendingAuthScope };