@metamask-previews/seedless-onboarding-controller 8.1.0-preview-152e02d → 8.1.0-preview-e0f8f1907

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,20 @@ 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
+ - Token refresh now uses a dedicated private `#reAuthenticate` method that only accepts the tokens returned by the JWT-refresh service (`idTokens`, `accessToken`, `metadataAccessToken`), making the re-authentication path stricter than the initial `authenticate` call ([#8148](https://github.com/MetaMask/core/pull/8148))
13
+ - After a successful JWT refresh the controller proactively rotates the refresh/revoke token pair (when the vault is unlocked) via the public `rotateRefreshToken` method ([#8148](https://github.com/MetaMask/core/pull/8148))
14
+ - Token expiry checks now use a 90% lifetime threshold for proactive refresh: access tokens and metadata access tokens are refreshed when less than 10% of their lifetime remains (requires `iat`), while node auth tokens fall back to exact-expiry checking ([#8148](https://github.com/MetaMask/core/pull/8148))
15
+
16
+ ### Removed
17
+
18
+ - **BREAKING:** Removed public `renewRefreshToken` method — refresh token rotation is now handled by `rotateRefreshToken`, which can also be called directly ([#8148](https://github.com/MetaMask/core/pull/8148))
19
+
20
+ ### Fixed
21
+
22
+ - Fixed `FailedToFetchAuthPubKey` errors caused by expired tokens in `addNewSecretData` and `changePassword` — `assertPasswordInSync` is now called inside the token-refresh wrapper so expired `nodeAuthTokens` are refreshed before reaching the backend
23
+
10
24
  ## [8.1.0]
11
25
 
12
26
  ### Changed
@@ -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_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_reAuthenticate, _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");
@@ -255,10 +255,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
255
255
  if (!metadataAccessToken) {
256
256
  throw new Error(constants_1.SeedlessOnboardingControllerErrorMessage.InvalidMetadataAccessToken);
257
257
  }
258
- // Check if token is expired and refresh if needed
258
+ // Use the same 90%-lifetime threshold as checkMetadataAccessTokenExpired
259
+ // so both code paths agree on when a refresh is needed.
259
260
  const decodedToken = (0, utils_3.decodeJWTToken)(metadataAccessToken);
260
- if (decodedToken.exp < Math.floor(Date.now() / 1000)) {
261
- // Token is expired, refresh it
261
+ if (isTokenNearExpiry(decodedToken.exp, decodedToken.iat)) {
262
+ // Token is near expiry or already expired, refresh it
262
263
  await this.refreshAuthTokens();
263
264
  // Get the new token after refresh
264
265
  const { metadataAccessToken: newMetadataAccessToken } = this.state;
@@ -291,7 +292,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
291
292
  * @param params.userId - user email or id from Social login
292
293
  * @param params.groupedAuthConnectionId - Optional grouped authConnectionId to be used for the authenticate request.
293
294
  * @param params.socialLoginEmail - The user email from Social login.
294
- * @param params.refreshToken - refresh token for refreshing expired nodeAuthTokens.
295
+ * @param params.refreshToken - Refresh token issued during OAuth login. Written to state when provided.
295
296
  * @param params.revokeToken - revoke token for revoking refresh token and get new refresh token and new revoke token.
296
297
  * @param params.accessToken - Access token for pairing with profile sync auth service and to access other services.
297
298
  * @param params.metadataAccessToken - Metadata access token for accessing the metadata service before the vault is created or unlocked.
@@ -397,11 +398,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
397
398
  async addNewSecretData(data, type, options) {
398
399
  return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
399
400
  __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsUnlocked).call(this);
400
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
401
- skipCache: true,
402
- skipLock: true, // skip lock since we already have the lock
403
- });
404
401
  const performBackup = async () => {
402
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
403
+ skipCache: true,
404
+ skipLock: true, // skip lock since we already have the lock
405
+ });
405
406
  // verify the password and unlock the vault
406
407
  const { toprfEncryptionKey, toprfAuthKeyPair } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this);
407
408
  // encrypt and store the secret data
@@ -476,11 +477,11 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
476
477
  await this.verifyVaultPassword(oldPassword, {
477
478
  skipLock: true, // skip lock since we already have the lock
478
479
  });
479
- const { latestKeyIndex } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
480
- skipCache: true,
481
- skipLock: true, // skip lock since we already have the lock
482
- });
483
480
  const attemptChangePassword = async () => {
481
+ const { latestKeyIndex } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertPasswordInSync).call(this, {
482
+ skipCache: true,
483
+ skipLock: true, // skip lock since we already have the lock
484
+ });
484
485
  // load keyring encryption key if it exists
485
486
  let keyringEncryptionKey;
486
487
  if (this.state.encryptedKeyringEncryptionKey) {
@@ -779,54 +780,64 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
779
780
  * @returns A promise that resolves when the tokens have been refreshed.
780
781
  */
781
782
  async refreshAuthTokens() {
783
+ // Coalesce concurrent calls to avoid issuing parallel HTTP requests
784
+ // with the same refresh token.
782
785
  if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f")) {
783
786
  return __classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f");
784
787
  }
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");
788
+ const promise = __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_doRefreshAuthTokens).call(this).finally(() => {
789
+ if (__classPrivateFieldGet(this, _SeedlessOnboardingController_pendingRefreshPromise, "f") === promise) {
790
+ __classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, undefined, "f");
791
+ }
792
+ });
793
+ __classPrivateFieldSet(this, _SeedlessOnboardingController_pendingRefreshPromise, promise, "f");
794
+ return promise;
789
795
  }
790
796
  /**
791
- * Renew the refresh token - get new refresh token and new revoke token
792
- * and also updates the vault with the new revoke token.
793
- * This method is to be called after user is authenticated.
797
+ * Rotate the refresh token — fetch a new refresh/revoke token pair from the
798
+ * auth service and persist the new revoke token in the vault.
799
+ *
800
+ * This method should be called after a successful JWT refresh.
794
801
  *
795
- * @param password - The password to encrypt the vault.
796
802
  * @returns A Promise that resolves to void.
797
803
  */
798
- async renewRefreshToken(password) {
799
- return await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_withControllerLock).call(this, async () => {
800
- __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
801
- const { refreshToken, vaultEncryptionKey } = this.state;
802
- const { toprfEncryptionKey: rawToprfEncryptionKey, toprfPwEncryptionKey: rawToprfPwEncryptionKey, toprfAuthKeyPair: rawToprfAuthKeyPair, revokeToken, } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_unlockVaultAndGetVaultData).call(this, {
803
- password,
804
- encryptionKey: vaultEncryptionKey,
804
+ async rotateRefreshToken() {
805
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_assertIsAuthenticatedUser).call(this, this.state);
806
+ const vaultData = __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f");
807
+ // Safety net: the caller (#doRefreshAuthTokens) already guards with
808
+ // `this.#isUnlocked && this.#cachedDecryptedVaultData`, so this branch
809
+ // is unreachable in normal flow. This check is added to satisfy the linter and
810
+ // to make sure any future caller of this method does not bypass the check.
811
+ /* istanbul ignore if */
812
+ if (!vaultData) {
813
+ throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.VaultLocked);
814
+ }
815
+ const { refreshToken } = this.state;
816
+ const { toprfPwEncryptionKey: pwEncKey, revokeToken } = vaultData;
817
+ const { newRevokeToken, newRefreshToken } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_renewRefreshToken, "f").call(this, {
818
+ connection: this.state.authConnection,
819
+ revokeToken,
820
+ });
821
+ if (newRevokeToken && newRefreshToken) {
822
+ // Persist the new revoke token to the vault first. Only update state after
823
+ // the vault write succeeds, so state and vault stay in sync if the write fails.
824
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_updateVault).call(this, {
825
+ vaultData: {
826
+ ...vaultData,
827
+ revokeToken: newRevokeToken,
828
+ },
829
+ pwEncKey,
830
+ });
831
+ this.update((state) => {
832
+ state.revokeToken = newRevokeToken;
833
+ state.refreshToken = newRefreshToken;
805
834
  });
806
- const { newRevokeToken, newRefreshToken } = await __classPrivateFieldGet(this, _SeedlessOnboardingController_renewRefreshToken, "f").call(this, {
807
- connection: this.state.authConnection,
835
+ // add the old refresh token to the list to be revoked later when possible
836
+ __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_addRefreshTokenToRevokeList).call(this, {
837
+ refreshToken,
808
838
  revokeToken,
809
839
  });
810
- if (newRevokeToken && newRefreshToken) {
811
- this.update((state) => {
812
- // set new revoke token in state temporarily for persisting in vault
813
- state.revokeToken = newRevokeToken;
814
- // set new refresh token to persist in state
815
- state.refreshToken = newRefreshToken;
816
- });
817
- await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_createNewVaultWithAuthData).call(this, {
818
- password,
819
- rawToprfEncryptionKey,
820
- rawToprfPwEncryptionKey,
821
- rawToprfAuthKeyPair,
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
- });
828
- }
829
- });
840
+ }
830
841
  }
831
842
  /**
832
843
  * Revoke all pending refresh tokens.
@@ -898,13 +909,15 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
898
909
  const firstAuthToken = nodeAuthTokens[0]?.authToken;
899
910
  // node auth token is base64 encoded json object
900
911
  const decodedToken = (0, utils_3.decodeNodeAuthToken)(firstAuthToken);
901
- // check if the token is expired
902
- return decodedToken.exp < Date.now() / 1000;
912
+ // Node auth tokens do not carry a reliable iat field — use exact expiry.
913
+ return isTokenNearExpiry(decodedToken.exp);
903
914
  }
904
915
  /**
905
- * Check if the current metadata access token is expired.
916
+ * Check if the current metadata access token should be refreshed.
917
+ * Returns true when the token is expired or when less than 10% of its
918
+ * lifetime remains (proactive refresh).
906
919
  *
907
- * @returns True if the metadata access token is expired, false otherwise.
920
+ * @returns True if the metadata access token should be refreshed, false otherwise.
908
921
  */
909
922
  checkMetadataAccessTokenExpired() {
910
923
  try {
@@ -912,17 +925,19 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
912
925
  const { metadataAccessToken } = this.state;
913
926
  // assertIsAuthenticatedUser will throw if metadataAccessToken is missing
914
927
  const decodedToken = (0, utils_3.decodeJWTToken)(metadataAccessToken);
915
- return decodedToken.exp < Math.floor(Date.now() / 1000);
928
+ return isTokenNearExpiry(decodedToken.exp, decodedToken.iat);
916
929
  }
917
930
  catch {
918
931
  return true; // Consider unauthenticated user as having expired tokens
919
932
  }
920
933
  }
921
934
  /**
922
- * Check if the current access token is expired.
935
+ * Check if the current access token should be refreshed.
936
+ * Returns true when the token is expired or when less than 10% of its
937
+ * lifetime remains (proactive refresh).
923
938
  * When the vault is locked, the access token is not accessible, so we return false.
924
939
  *
925
- * @returns True if the access token is expired, false otherwise.
940
+ * @returns True if the access token should be refreshed, false otherwise.
926
941
  */
927
942
  checkAccessTokenExpired() {
928
943
  try {
@@ -932,7 +947,7 @@ class SeedlessOnboardingController extends base_controller_1.BaseController {
932
947
  return true; // Consider missing token as expired
933
948
  }
934
949
  const decodedToken = (0, utils_3.decodeJWTToken)(accessToken);
935
- return decodedToken.exp < Math.floor(Date.now() / 1000);
950
+ return isTokenNearExpiry(decodedToken.exp, decodedToken.iat);
936
951
  }
937
952
  catch {
938
953
  return true; // Consider unauthenticated user as having expired tokens
@@ -1619,25 +1634,12 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
1619
1634
  });
1620
1635
  try {
1621
1636
  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({
1637
+ // Re-authenticate with the refreshed id tokens to update node auth
1638
+ // tokens, accessToken, and metadataAccessToken in state.
1639
+ await __classPrivateFieldGet(this, _SeedlessOnboardingController_instances, "m", _SeedlessOnboardingController_reAuthenticate).call(this, {
1632
1640
  idTokens,
1633
1641
  accessToken,
1634
1642
  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
1643
  });
1642
1644
  // update the vault with new access token if wallet is unlocked
1643
1645
  if (__classPrivateFieldGet(this, _SeedlessOnboardingController_isUnlocked, "f") && __classPrivateFieldGet(this, _SeedlessOnboardingController_cachedDecryptedVaultData, "f")) {
@@ -1650,6 +1652,15 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
1650
1652
  vaultData: updatedVaultData,
1651
1653
  pwEncKey,
1652
1654
  });
1655
+ // Proactively rotate the refresh token now that we have vault access.
1656
+ await this.rotateRefreshToken().catch((error) => {
1657
+ // Rotation failure is intentionally non-fatal: the JWT refresh
1658
+ // itself succeeded and the caller should not be blocked.
1659
+ // However the user is now operating with a stale refresh token
1660
+ // that may be revoked server-side, so log prominently.
1661
+ log('Failed to rotate refresh token after successful JWT refresh. ' +
1662
+ 'The user may be logged out when the old token is revoked.', error);
1663
+ });
1653
1664
  }
1654
1665
  }
1655
1666
  catch (error) {
@@ -1658,6 +1669,58 @@ async function _SeedlessOnboardingController_doRefreshAuthTokens() {
1658
1669
  cause: error,
1659
1670
  });
1660
1671
  }
1672
+ }, _SeedlessOnboardingController_reAuthenticate =
1673
+ /**
1674
+ * Re-authenticate the user using freshly issued tokens from a JWT refresh.
1675
+ *
1676
+ * Unlike the public `authenticate` method, this variant is called exclusively
1677
+ * from token-refresh paths where the user's identity has already been
1678
+ * established. It only accepts the tokens returned by the JWT-refresh
1679
+ * service (`idTokens`, `accessToken`, `metadataAccessToken`) and reads all
1680
+ * other auth-connection details from the existing controller state.
1681
+ *
1682
+ * `refreshToken` and `revokeToken` are intentionally absent — token-refresh
1683
+ * paths must not touch `state.refreshToken` directly; that field is managed
1684
+ * exclusively by `rotateRefreshToken`.
1685
+ *
1686
+ * @param params - Tokens issued by the JWT-refresh service.
1687
+ * @param params.idTokens - New node id tokens.
1688
+ * @param params.accessToken - New access token.
1689
+ * @param params.metadataAccessToken - New metadata access token.
1690
+ * @returns A promise that resolves to the authentication result.
1691
+ */
1692
+ async function _SeedlessOnboardingController_reAuthenticate(params) {
1693
+ const { idTokens, accessToken, metadataAccessToken } = params;
1694
+ const { authConnection, authConnectionId, groupedAuthConnectionId, userId, socialLoginEmail, } = this.state;
1695
+ try {
1696
+ const authenticationResult = await this.toprfClient.authenticate({
1697
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1698
+ authConnectionId: authConnectionId,
1699
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1700
+ userId: userId,
1701
+ idTokens,
1702
+ groupedAuthConnectionId,
1703
+ });
1704
+ this.update((state) => {
1705
+ state.nodeAuthTokens = authenticationResult.nodeAuthTokens;
1706
+ state.authConnection = authConnection;
1707
+ state.authConnectionId = authConnectionId;
1708
+ state.groupedAuthConnectionId = groupedAuthConnectionId;
1709
+ state.userId = userId;
1710
+ state.socialLoginEmail = socialLoginEmail;
1711
+ state.metadataAccessToken = metadataAccessToken;
1712
+ state.accessToken = accessToken;
1713
+ (0, assertions_1.assertIsSeedlessOnboardingUserAuthenticated)(state);
1714
+ state.isSeedlessOnboardingUserAuthenticated = true;
1715
+ });
1716
+ return authenticationResult;
1717
+ }
1718
+ catch (error) {
1719
+ log('Error re-authenticating user', error);
1720
+ throw new errors_1.SeedlessOnboardingError(constants_1.SeedlessOnboardingControllerErrorMessage.AuthenticationError, {
1721
+ cause: error,
1722
+ });
1723
+ }
1661
1724
  }, _SeedlessOnboardingController_addRefreshTokenToRevokeList = function _SeedlessOnboardingController_addRefreshTokenToRevokeList({ refreshToken, revokeToken, }) {
1662
1725
  this.update((state) => {
1663
1726
  state.pendingToBeRevokedTokens = [
@@ -1732,6 +1795,32 @@ async function _SeedlessOnboardingController_executeWithTokenRefresh(operation,
1732
1795
  isMetadataAccessTokenExpired ||
1733
1796
  isAccessTokenExpired);
1734
1797
  };
1798
+ /**
1799
+ * Determine whether a token should be proactively refreshed.
1800
+ *
1801
+ * When `iat` is provided: returns `true` when less than 10% of the token's
1802
+ * lifetime remains (i.e. we are in the last 10% before expiry).
1803
+ * When `iat` is omitted (e.g. node auth tokens): returns `true` when the token
1804
+ * is already expired.
1805
+ *
1806
+ * @param exp - Token expiration time in seconds (Unix epoch).
1807
+ * @param iat - Optional issued-at time in seconds (Unix epoch). Required for 10% threshold.
1808
+ * @returns True if the token should be refreshed.
1809
+ */
1810
+ function isTokenNearExpiry(exp, iat) {
1811
+ const now = Date.now() / 1000;
1812
+ if (iat === undefined) {
1813
+ return now >= exp;
1814
+ }
1815
+ const lifetime = exp - iat;
1816
+ // Guard against malformed tokens where iat >= exp (zero or negative lifetime).
1817
+ // Fall back to exact-expiry check so bad tokens are always considered stale.
1818
+ if (lifetime <= 0) {
1819
+ return now >= exp;
1820
+ }
1821
+ const remaining = exp - now;
1822
+ return remaining <= 0.1 * lifetime;
1823
+ }
1735
1824
  /**
1736
1825
  * Lock the given mutex before executing the given function,
1737
1826
  * and release it after the function is resolved or after an