@metamask-previews/seedless-onboarding-controller 8.0.0-preview-511b467 → 8.0.0-preview-a891ec3b5

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,17 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ### Changed
11
-
12
- - `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))
13
- - `revokePendingRefreshTokens` now enforces a per-token grace period before revocation — only tokens that have been pending for at least `PENDING_REVOKE_TOKEN_DELAY_MS` (15 s) are eligible, preventing a cross-cycle race where an earlier rotation cycle revokes tokens queued by a later cycle ([#7989](https://github.com/MetaMask/core/pull/7989))
14
- - `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))
15
- - `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))
16
-
17
- ### Fixed
18
-
19
- - 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))
20
-
21
10
  ## [8.0.0]
22
11
 
23
12
  ### Added
@@ -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_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;
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;
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,12 +212,6 @@ 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);
221
215
  _SeedlessOnboardingController_refreshJWTToken.set(this, void 0);
222
216
  _SeedlessOnboardingController_revokeRefreshToken.set(this, void 0);
223
217
  _SeedlessOnboardingController_renewRefreshToken.set(this, void 0);
@@ -769,23 +763,59 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
769
763
  return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_loadKeyringEncryptionKey).call(this, encKey);
770
764
  }
771
765
  /**
772
- * Refresh expired nodeAuthTokens, accessToken, and metadataAccessToken using
773
- * the stored refresh token.
766
+ * Refresh expired nodeAuthTokens, accessToken, and metadataAccessToken using the stored refresh token.
774
767
  *
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.
768
+ * This method retrieves the refresh token from the vault and uses it to obtain
769
+ * new nodeAuthTokens when the current ones have expired.
778
770
  *
779
- * @returns A promise that resolves when the tokens have been refreshed.
771
+ * @returns A promise that resolves to the new nodeAuthTokens.
780
772
  */
781
773
  async refreshAuthTokens() {
782
- if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f")) {
783
- return __classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f");
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
+ });
784
818
  }
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");
789
819
  }
790
820
  /**
791
821
  * Renew the refresh token - get new refresh token and new revoke token
@@ -814,17 +844,17 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
814
844
  // set new refresh token to persist in state
815
845
  state.refreshToken = newRefreshToken;
816
846
  });
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
+ });
817
852
  await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
818
853
  password,
819
854
  rawToprfEncryptionKey,
820
855
  rawToprfPwEncryptionKey,
821
856
  rawToprfAuthKeyPair,
822
857
  });
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
- });
828
858
  }
829
859
  });
830
860
  }
@@ -842,17 +872,8 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
842
872
  if (!pendingToBeRevokedTokens || pendingToBeRevokedTokens.length === 0) {
843
873
  return;
844
874
  }
845
- // Only revoke tokens that have been pending for at least PENDING_REVOKE_TOKEN_DELAY_MS.
846
- // Entries without queuedAt (pre-migration persisted state) default to 0 (epoch) and
847
- // are always old enough. This prevents a cross-cycle race where an earlier cycle's
848
- // timer revokes tokens queued by a later cycle before those tokens' own grace period expires.
849
- const now = Date.now();
850
- const tokensReadyToRevoke = pendingToBeRevokedTokens.filter(({ queuedAt }) => now - (queuedAt ?? 0) >= constants_1.PENDING_REVOKE_TOKEN_DELAY_MS);
851
- if (tokensReadyToRevoke.length === 0) {
852
- return;
853
- }
854
875
  // revoke all pending refresh tokens in parallel
855
- const promises = tokensReadyToRevoke.map(({ revokeToken }) => {
876
+ const promises = pendingToBeRevokedTokens.map(({ revokeToken }) => {
856
877
  const revokePromise = async () => {
857
878
  try {
858
879
  await __classPrivateFieldGet(this, _SeedlessOnboardingController_revokeRefreshToken, "f").call(this, {
@@ -949,7 +970,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
949
970
  }
950
971
  }
951
972
  exports.SeedlessOnboardingController = SeedlessOnboardingController;
952
- _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 =
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 =
953
974
  /**
954
975
  * @description Submit the global password to the controller, verify the
955
976
  * password validity and unlock the controller.
@@ -1599,79 +1620,11 @@ async function _SeedlessOnboardingController_assertPasswordInSync(options) {
1599
1620
  this.update((state) => {
1600
1621
  delete state.passwordOutdatedCache;
1601
1622
  });
1602
- }, _SeedlessOnboardingController_doRefreshAuthTokens =
1603
- /**
1604
- * Internal implementation of token refresh. Called exclusively by
1605
- * `refreshAuthTokens` which gates concurrent access via
1606
- * `#pendingRefreshPromise`.
1607
- */
1608
- async function _SeedlessOnboardingController_doRefreshAuthTokens() {
1609
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
1610
- const { refreshToken } = this.state;
1611
- const res = await __classPrivateFieldGet(this, _SeedlessOnboardingController_refreshJWTToken, "f").call(this, {
1612
- connection: this.state.authConnection,
1613
- refreshToken,
1614
- }).catch((error) => {
1615
- // Distinguish a server-side token rejection (401) from transient
1616
- // failures so callers can apply the appropriate recovery strategy.
1617
- const httpStatusCode = error
1618
- .statusCode;
1619
- log('Error refreshing JWT tokens', error, { httpStatusCode });
1620
- const isTokenRevoked = error instanceof Error &&
1621
- error.name === 'RefreshTokenHttpError' &&
1622
- httpStatusCode === 401;
1623
- throw new errors_1.SeedlessOnboardingError(isTokenRevoked
1624
- ? constants_1.SeedlessOnboardingControllerErrorMessage.InvalidRefreshToken
1625
- : constants_1.SeedlessOnboardingControllerErrorMessage.FailedToRefreshJWTTokens, {
1626
- cause: error,
1627
- });
1628
- });
1629
- try {
1630
- const { idTokens, accessToken, metadataAccessToken } = res;
1631
- // re-authenticate with the new id tokens to set new node auth tokens
1632
- // 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
1633
- // NOTE: use this.state.refreshToken (current value) rather than the
1634
- // `refreshToken` captured at the start of this method. If renewRefreshToken()
1635
- // ran concurrently while our HTTP call was in-flight, it will have already
1636
- // updated state.refreshToken to the new token. Using the captured (now-stale)
1637
- // value would overwrite that newer token and revert the state, causing the
1638
- // old token to be used for subsequent refreshes — and once that old token is
1639
- // revoked (15 s after the renewal), every subsequent refresh will return 401.
1640
- await this.authenticate({
1641
- idTokens,
1642
- accessToken,
1643
- metadataAccessToken,
1644
- authConnection: this.state.authConnection,
1645
- authConnectionId: this.state.authConnectionId,
1646
- groupedAuthConnectionId: this.state.groupedAuthConnectionId,
1647
- userId: this.state.userId,
1648
- refreshToken: this.state.refreshToken,
1649
- skipLock: true,
1650
- });
1651
- // update the vault with new access token if wallet is unlocked
1652
- if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f") && __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
1653
- const updatedVaultData = {
1654
- ...__classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f"),
1655
- accessToken,
1656
- };
1657
- const pwEncKey = __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f").toprfPwEncryptionKey;
1658
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
1659
- vaultData: updatedVaultData,
1660
- pwEncKey,
1661
- });
1662
- }
1663
- }
1664
- catch (error) {
1665
- log('Error refreshing node auth tokens', error);
1666
- throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.AuthenticationError, {
1667
- cause: error,
1668
- });
1669
- }
1670
1623
  }, _SeedlessOnboardingController_addRefreshTokenToRevokeList = function _SeedlessOnboardingController_addRefreshTokenToRevokeList({ refreshToken, revokeToken, }) {
1671
1624
  this.update((state) => {
1672
1625
  state.pendingToBeRevokedTokens = [
1673
1626
  ...(state.pendingToBeRevokedTokens ?? []),
1674
- { refreshToken, revokeToken, queuedAt: Date.now() },
1627
+ { refreshToken, revokeToken },
1675
1628
  ];
1676
1629
  });
1677
1630
  }, _SeedlessOnboardingController_isAuthTokenError = function _SeedlessOnboardingController_isAuthTokenError(error) {