From 9b75c3f3864baa7d90c94ce57b290c2a7cdbd949 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 1 Oct 2026 10:52:52 +0200 Subject: [PATCH 1/3] fix: coalesce sign ins --- packages/profile-sync-controller/CHANGELOG.md | 1 + .../AuthenticationController.test.ts | 20 +++++++++++++++++++ .../AuthenticationController.ts | 19 ++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index e371bbd9083..e3f51712288 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Coalesce overlapping `performSignIn` calls so only one sign-in runs at a time ([#XXX](https://github.com/MetaMask/core/pull/XXX)) - Rely on `@metamask/key-tree` crypto implementation for HMAC-SHA-512 instead of hardcoded `noble` implementation ([#10424](https://github.com/MetaMask/core/pull/10424)) - `@metamask/key-tree` uses WebCrypto API if available and fallback to `noble` otherwise. - One note, we expect the platform to provide a fully-compliant WebCrypto (`crypto.subtle`) implementation for this to work (`@metamask/key-tree` detection is global and not "per crypto functions"). diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts index cee2884326d..8632da513a1 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.test.ts @@ -596,6 +596,26 @@ describe('AuthenticationController', () => { expect(controller.state.needsProfilePairing).toBe(false); }); + it('shares one in-flight performSignIn across overlapping callers', async () => { + const metametrics = createMockAuthMetaMetrics(); + arrangeAuthAPIs({ mockPairProfilesDelayMs: 50 }); + const { messenger } = createMockAuthenticationMessenger(); + + const controller = new AuthenticationController({ + messenger, + metametrics, + }); + + const first = controller.performSignIn(); + const second = controller.performSignIn(); + const [firstTokens, secondTokens] = await Promise.all([first, second]); + + expect(secondTokens).toBe(firstTokens); + + const thirdTokens = await controller.performSignIn(); + expect(thirdTokens).not.toBe(firstTokens); + }); + it('does NOT throw out of performSignIn when pairProfiles fails, leaves needsProfilePairing=true', async () => { const metametrics = createMockAuthMetaMetrics(); arrangeAuthAPIs({ diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts index d73cb4cdb4e..79821b7e3af 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts @@ -333,6 +333,11 @@ export class AuthenticationController extends BaseController< */ #profilePairingRequestEpoch = 0; + /** + * Sign-in currently running. Overlapping callers share it. + */ + #ongoingSignIn?: Promise; + readonly #keyringController = { setupLockedStateSubscriptions: () => { const { isUnlocked } = this.messenger.call('KeyringController:getState'); @@ -596,7 +601,21 @@ export class AuthenticationController extends BaseController< public async performSignIn(): Promise { this.#assertIsUnlocked('performSignIn'); + if (this.#ongoingSignIn) { + return await this.#ongoingSignIn; + } + + const signIn = this.#runPerformSignIn().finally(() => { + if (this.#ongoingSignIn === signIn) { + this.#ongoingSignIn = undefined; + } + }); + // Assigned before this method awaits, so a second caller joins this flight. + this.#ongoingSignIn = signIn; + return await signIn; + } + async #runPerformSignIn(): Promise { const epochAtStart = this.#profilePairingRequestEpoch; const entropySourceIds = this.#getHdKeyringEntropySourceIds(); const accessTokens: string[] = []; From 45601470c58058051294283151c1a410c352f234 Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 1 Oct 2026 10:54:59 +0200 Subject: [PATCH 2/3] fix: update CHANGELOGs --- packages/profile-sync-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index e3f51712288..d66c90c8213 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Coalesce overlapping `performSignIn` calls so only one sign-in runs at a time ([#XXX](https://github.com/MetaMask/core/pull/XXX)) +- Coalesce overlapping `performSignIn` calls so only one sign-in runs at a time ([#10646](https://github.com/MetaMask/core/pull/10646)) - Rely on `@metamask/key-tree` crypto implementation for HMAC-SHA-512 instead of hardcoded `noble` implementation ([#10424](https://github.com/MetaMask/core/pull/10424)) - `@metamask/key-tree` uses WebCrypto API if available and fallback to `noble` otherwise. - One note, we expect the platform to provide a fully-compliant WebCrypto (`crypto.subtle`) implementation for this to work (`@metamask/key-tree` detection is global and not "per crypto functions"). From 576eebcfebc63c2a1898ee43dac110c3da047ffd Mon Sep 17 00:00:00 2001 From: Mathieu Artu Date: Thu, 1 Oct 2026 10:56:07 +0200 Subject: [PATCH 3/3] fix: remove unnecessary comment --- .../src/controllers/authentication/AuthenticationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts index 79821b7e3af..bce495dfd0e 100644 --- a/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts +++ b/packages/profile-sync-controller/src/controllers/authentication/AuthenticationController.ts @@ -610,7 +610,7 @@ export class AuthenticationController extends BaseController< this.#ongoingSignIn = undefined; } }); - // Assigned before this method awaits, so a second caller joins this flight. + this.#ongoingSignIn = signIn; return await signIn; }