@metamask-previews/seedless-onboarding-controller 8.0.0-preview-06d68e653 → 8.1.0-preview-d1f62d044

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
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [8.1.0]
11
+
12
+ ### Changed
13
+
14
+ - `refreshAuthTokens` now coalesces concurrent calls — if a refresh is already in-flight, subsequent callers share the same promise rather than issuing duplicate HTTP requests with the same token ([#7989](https://github.com/MetaMask/core/pull/7989))
15
+ - `refreshAuthTokens` now uses the live `state.refreshToken` value when calling `authenticate` after a successful HTTP refresh, preventing a stale-capture bug that could overwrite a concurrently-renewed token with an older one ([#7989](https://github.com/MetaMask/core/pull/7989))
16
+ - `renewRefreshToken` now queues the old token for revocation only after `#createNewVaultWithAuthData` succeeds, ensuring tokens are not queued if vault creation fails ([#7989](https://github.com/MetaMask/core/pull/7989))
17
+
18
+ ### Fixed
19
+
20
+ - Fixed `refreshAuthTokens` throwing the generic `FailedToRefreshJWTTokens` error for all HTTP failures — a 401 response (permanently revoked token) now throws `InvalidRefreshToken` instead, giving callers a distinct signal to distinguish permanent from transient failures ([#7989](https://github.com/MetaMask/core/pull/7989))
21
+
10
22
  ## [8.0.0]
11
23
 
12
24
  ### Added
@@ -280,7 +292,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
280
292
  - `checkIsPasswordOutdated`: Check if the password is current device is outdated, i.e. user changed password in another device.
281
293
  - `clearState`: Reset the state of the controller to the defaults.
282
294
 
283
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@8.0.0...HEAD
295
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@8.1.0...HEAD
296
+ [8.1.0]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@8.0.0...@metamask/seedless-onboarding-controller@8.1.0
284
297
  [8.0.0]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@7.1.0...@metamask/seedless-onboarding-controller@8.0.0
285
298
  [7.1.0]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@7.0.0...@metamask/seedless-onboarding-controller@7.1.0
286
299
  [7.0.0]: https://github.com/MetaMask/core/compare/@metamask/seedless-onboarding-controller@6.1.0...@metamask/seedless-onboarding-controller@7.0.0
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_pickLatestAccessToken, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_encryptSeedlessEncryptionKey, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh, _SeedlessOnboardingController_checkTokensExpired;
13
+ var _SeedlessOnboardingController_instances, _SeedlessOnboardingController_vaultEncryptor, _SeedlessOnboardingController_controllerOperationMutex, _SeedlessOnboardingController_vaultOperationMutex, _SeedlessOnboardingController_pendingRefreshPromise, _SeedlessOnboardingController_refreshJWTToken, _SeedlessOnboardingController_revokeRefreshToken, _SeedlessOnboardingController_renewRefreshToken, _SeedlessOnboardingController_passwordOutdatedCacheTTL, _SeedlessOnboardingController_isUnlocked, _SeedlessOnboardingController_cachedDecryptedVaultData, _SeedlessOnboardingController_submitGlobalPassword, _SeedlessOnboardingController_setUnlocked, _SeedlessOnboardingController_pickLatestAccessToken, _SeedlessOnboardingController_persistOprfKey, _SeedlessOnboardingController_persistAuthPubKey, _SeedlessOnboardingController_storeKeyringEncryptionKey, _SeedlessOnboardingController_loadKeyringEncryptionKey, _SeedlessOnboardingController_loadSeedlessEncryptionKey, _SeedlessOnboardingController_recoverAuthPubKey, _SeedlessOnboardingController_recoverEncKey, _SeedlessOnboardingController_fetchAllSecretDataFromMetadataStore, _SeedlessOnboardingController_changeEncryptionKey, _SeedlessOnboardingController_encryptAndStoreSecretData, _SeedlessOnboardingController_unlockVaultAndGetVaultData, _SeedlessOnboardingController_decryptAndParseVaultData, _SeedlessOnboardingController_withPersistedSecretMetadataBackupsState, _SeedlessOnboardingController_filterDupesAndUpdateSocialBackupsMetadata, _SeedlessOnboardingController_createNewVaultWithAuthData, _SeedlessOnboardingController_updateVault, _SeedlessOnboardingController_encryptSeedlessEncryptionKey, _SeedlessOnboardingController_getAccessTokenAndRevokeToken, _SeedlessOnboardingController_withControllerLock, _SeedlessOnboardingController_withVaultLock, _SeedlessOnboardingController_parseVaultData, _SeedlessOnboardingController_assertIsUnlocked, _SeedlessOnboardingController_assertIsAuthenticatedUser, _SeedlessOnboardingController_assertPasswordInSync, _SeedlessOnboardingController_resetPasswordOutdatedCache, _SeedlessOnboardingController_doRefreshAuthTokens, _SeedlessOnboardingController_addRefreshTokenToRevokeList, _SeedlessOnboardingController_isAuthTokenError, _SeedlessOnboardingController_isMaxKeyChainLengthError, _SeedlessOnboardingController_executeWithTokenRefresh, _SeedlessOnboardingController_checkTokensExpired;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.SeedlessOnboardingController = exports.getInitialSeedlessOnboardingControllerStateWithDefaults = void 0;
16
16
  const auth_network_utils_1 = require("@metamask/auth-network-utils");
@@ -212,6 +212,12 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
212
212
  _SeedlessOnboardingController_vaultEncryptor.set(this, void 0);
213
213
  _SeedlessOnboardingController_controllerOperationMutex.set(this, new async_mutex_1.Mutex());
214
214
  _SeedlessOnboardingController_vaultOperationMutex.set(this, new async_mutex_1.Mutex());
215
+ /**
216
+ * In-flight promise for `refreshAuthTokens`. Any concurrent caller that
217
+ * arrives while a refresh is already in-progress will share this promise
218
+ * rather than issuing a second HTTP request with the same refresh token.
219
+ */
220
+ _SeedlessOnboardingController_pendingRefreshPromise.set(this, void 0);
215
221
  _SeedlessOnboardingController_refreshJWTToken.set(this, void 0);
216
222
  _SeedlessOnboardingController_revokeRefreshToken.set(this, void 0);
217
223
  _SeedlessOnboardingController_renewRefreshToken.set(this, void 0);
@@ -763,59 +769,23 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
763
769
  return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_loadKeyringEncryptionKey).call(this, encKey);
764
770
  }
765
771
  /**
766
- * Refresh expired nodeAuthTokens, accessToken, and metadataAccessToken using the stored refresh token.
772
+ * Refresh expired nodeAuthTokens, accessToken, and metadataAccessToken using
773
+ * the stored refresh token.
767
774
  *
768
- * This method retrieves the refresh token from the vault and uses it to obtain
769
- * new nodeAuthTokens when the current ones have expired.
775
+ * Concurrent callers share a single in-flight HTTP request — if a refresh is
776
+ * already in-progress the returned promise resolves when that request settles
777
+ * rather than firing a duplicate request with the same token.
770
778
  *
771
- * @returns A promise that resolves to the new nodeAuthTokens.
779
+ * @returns A promise that resolves when the tokens have been refreshed.
772
780
  */
773
781
  async refreshAuthTokens() {
774
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
775
- const { refreshToken } = this.state;
776
- const res = await __classPrivateFieldGet(this, _SeedlessOnboardingController_refreshJWTToken, "f").call(this, {
777
- connection: this.state.authConnection,
778
- refreshToken,
779
- }).catch((error) => {
780
- log('Error refreshing JWT tokens', error);
781
- throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, {
782
- cause: error,
783
- });
784
- });
785
- try {
786
- const { idTokens, accessToken, metadataAccessToken } = res;
787
- // re-authenticate with the new id tokens to set new node auth tokens
788
- // NOTE: here we can't provide the `revokeToken` value to the `authenticate` method because `refreshAuthTokens` method can be called when the wallet (vault) is locked
789
- await this.authenticate({
790
- idTokens,
791
- accessToken,
792
- metadataAccessToken,
793
- authConnection: this.state.authConnection,
794
- authConnectionId: this.state.authConnectionId,
795
- groupedAuthConnectionId: this.state.groupedAuthConnectionId,
796
- userId: this.state.userId,
797
- refreshToken,
798
- skipLock: true,
799
- });
800
- // update the vault with new access token if wallet is unlocked
801
- if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f") && __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
802
- const updatedVaultData = {
803
- ...__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f"),
804
- accessToken,
805
- };
806
- const pwEncKey = __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f").toprfPwEncryptionKey;
807
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
808
- vaultData: updatedVaultData,
809
- pwEncKey,
810
- });
811
- }
812
- }
813
- catch (error) {
814
- log('Error refreshing node auth tokens', error);
815
- throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.AuthenticationError, {
816
- cause: error,
817
- });
782
+ if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f")) {
783
+ return __classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f");
818
784
  }
785
+ __classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_doRefreshAuthTokens).call(this).finally(() => {
786
+ __classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, undefined, "f");
787
+ }), "f");
788
+ return __classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f");
819
789
  }
820
790
  /**
821
791
  * Renew the refresh token - get new refresh token and new revoke token
@@ -844,17 +814,17 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
844
814
  // set new refresh token to persist in state
845
815
  state.refreshToken = newRefreshToken;
846
816
  });
847
- // add the old refresh token to the list to be revoked later when possible
848
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_addRefreshTokenToRevokeList).call(this, {
849
- refreshToken,
850
- revokeToken,
851
- });
852
817
  await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
853
818
  password,
854
819
  rawToprfEncryptionKey,
855
820
  rawToprfPwEncryptionKey,
856
821
  rawToprfAuthKeyPair,
857
822
  });
823
+ // add the old refresh token to the list to be revoked later when possible
824
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_addRefreshTokenToRevokeList).call(this, {
825
+ refreshToken,
826
+ revokeToken,
827
+ });
858
828
  }
859
829
  });
860
830
  }
@@ -970,7 +940,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
970
940
  }
971
941
  }
972
942
  exports.SeedlessOnboardingController = SeedlessOnboardingController;
973
- _SeedlessOnboardingController_vaultEncryptor = new WeakMap(), _SeedlessOnboardingController_controllerOperationMutex = new WeakMap(), _SeedlessOnboardingController_vaultOperationMutex = new WeakMap(), _SeedlessOnboardingController_refreshJWTToken = new WeakMap(), _SeedlessOnboardingController_revokeRefreshToken = new WeakMap(), _SeedlessOnboardingController_renewRefreshToken = new WeakMap(), _SeedlessOnboardingController_passwordOutdatedCacheTTL = new WeakMap(), _SeedlessOnboardingController_isUnlocked = new WeakMap(), _SeedlessOnboardingController_cachedDecryptedVaultData = new WeakMap(), _SeedlessOnboardingController_instances = new WeakSet(), _SeedlessOnboardingController_submitGlobalPassword =
943
+ _SeedlessOnboardingController_vaultEncryptor = new WeakMap(), _SeedlessOnboardingController_controllerOperationMutex = new WeakMap(), _SeedlessOnboardingController_vaultOperationMutex = new WeakMap(), _SeedlessOnboardingController_pendingRefreshPromise = new WeakMap(), _SeedlessOnboardingController_refreshJWTToken = new WeakMap(), _SeedlessOnboardingController_revokeRefreshToken = new WeakMap(), _SeedlessOnboardingController_renewRefreshToken = new WeakMap(), _SeedlessOnboardingController_passwordOutdatedCacheTTL = new WeakMap(), _SeedlessOnboardingController_isUnlocked = new WeakMap(), _SeedlessOnboardingController_cachedDecryptedVaultData = new WeakMap(), _SeedlessOnboardingController_instances = new WeakSet(), _SeedlessOnboardingController_submitGlobalPassword =
974
944
  /**
975
945
  * @description Submit the global password to the controller, verify the
976
946
  * password validity and unlock the controller.
@@ -1620,6 +1590,74 @@ async function _SeedlessOnboardingController_assertPasswordInSync(options) {
1620
1590
  this.update((state) => {
1621
1591
  delete state.passwordOutdatedCache;
1622
1592
  });
1593
+ }, _SeedlessOnboardingController_doRefreshAuthTokens =
1594
+ /**
1595
+ * Internal implementation of token refresh. Called exclusively by
1596
+ * `refreshAuthTokens` which gates concurrent access via
1597
+ * `#pendingRefreshPromise`.
1598
+ */
1599
+ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
1600
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
1601
+ const { refreshToken } = this.state;
1602
+ const res = await __classPrivateFieldGet(this, _SeedlessOnboardingController_refreshJWTToken, "f").call(this, {
1603
+ connection: this.state.authConnection,
1604
+ refreshToken,
1605
+ }).catch((error) => {
1606
+ // Distinguish a server-side token rejection (401) from transient
1607
+ // failures so callers can apply the appropriate recovery strategy.
1608
+ const httpStatusCode = error
1609
+ .statusCode;
1610
+ log('Error refreshing JWT tokens', error, { httpStatusCode });
1611
+ const isTokenRevoked = error instanceof Error &&
1612
+ error.name === 'RefreshTokenHttpError' &&
1613
+ httpStatusCode === 401;
1614
+ throw new errors_1.SeedlessOnboardingError(isTokenRevoked
1615
+ ? constants_1.SeedlessOnboardingControllerErrorMessage.InvalidRefreshToken
1616
+ : constants_1.SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, {
1617
+ cause: error,
1618
+ });
1619
+ });
1620
+ try {
1621
+ const { idTokens, accessToken, metadataAccessToken } = res;
1622
+ // re-authenticate with the new id tokens to set new node auth tokens
1623
+ // NOTE: here we can't provide the `revokeToken` value to the `authenticate` method because `refreshAuthTokens` method can be called when the wallet (vault) is locked
1624
+ // NOTE: use this.state.refreshToken (current value) rather than the
1625
+ // `refreshToken` captured at the start of this method. If renewRefreshToken()
1626
+ // ran concurrently while our HTTP call was in-flight, it will have already
1627
+ // updated state.refreshToken to the new token. Using the captured (now-stale)
1628
+ // value would overwrite that newer token and revert the state, causing the
1629
+ // old token to be used for subsequent refreshes — and once that old token is
1630
+ // revoked (15 s after the renewal), every subsequent refresh will return 401.
1631
+ await this.authenticate({
1632
+ idTokens,
1633
+ accessToken,
1634
+ metadataAccessToken,
1635
+ authConnection: this.state.authConnection,
1636
+ authConnectionId: this.state.authConnectionId,
1637
+ groupedAuthConnectionId: this.state.groupedAuthConnectionId,
1638
+ userId: this.state.userId,
1639
+ refreshToken: this.state.refreshToken,
1640
+ skipLock: true,
1641
+ });
1642
+ // update the vault with new access token if wallet is unlocked
1643
+ if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f") && __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
1644
+ const updatedVaultData = {
1645
+ ...__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f"),
1646
+ accessToken,
1647
+ };
1648
+ const pwEncKey = __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f").toprfPwEncryptionKey;
1649
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
1650
+ vaultData: updatedVaultData,
1651
+ pwEncKey,
1652
+ });
1653
+ }
1654
+ }
1655
+ catch (error) {
1656
+ log('Error refreshing node auth tokens', error);
1657
+ throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.AuthenticationError, {
1658
+ cause: error,
1659
+ });
1660
+ }
1623
1661
  }, _SeedlessOnboardingController_addRefreshTokenToRevokeList = function _SeedlessOnboardingController_addRefreshTokenToRevokeList({ refreshToken, revokeToken, }) {
1624
1662
  this.update((state) => {
1625
1663
  state.pendingToBeRevokedTokens = [