Skip to content

feat(profile-controller): backend-owned X OAuth connect and disconnect - #10668

Merged
montelaidev merged 15 commits into
mainfrom
feat/x-integration
Oct 12, 2026
Merged

montelaidev merged 15 commits into
mainfrom
feat/x-integration

Conversation

@montelaidev

@montelaidev montelaidev commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Explanation

@metamask/profile-controller could start an X (Twitter) link, but the client had to supply the profile ID and hold the OAuth details. The Profile API now owns the whole X OAuth flow: it keeps the client secret, the PKCE verifier and the X tokens, and it creates the MetaMask profile during the connect if one doesn't exist yet (using the X handle as the username). The client only opens the authorize URL and relays the returned code and state.

This PR updates the controller and service to match:

  • ProfileController.startXConnect({ linkedAddress? }) (new) returns { authorizationUrl, state }. Nothing is stored in controller state. The profile comes from the verified bearer token on the backend. linkedAddress (CAIP-10) is forwarded as linked_address and is required when the profile doesn't exist yet.
  • ProfileController.connectX({ code, state, profileId }) (BREAKING) now returns { profile, xProfile, profileCreated } instead of just the X profile.
    • It saves xProfile to state as soon as the connect succeeds, then fetches the profile, because the backend may have just created it.
    • If that fetch fails, the X link stays saved and a clear error is thrown.
  • ProfileController.disconnectX(profileId) (new) clears xProfile and sets connectedToX to false.
  • ProfileService.disconnectX(profileId) (new) sends DELETE profiles/{profileId}/x. A 404 is treated as success, meaning the account is already disconnected; otherwise a client whose saved state has drifted from the backend could never disconnect.
  • ProfileService.getXAuthUrl(linkedAddress?) sends the optional linked_address query parameter.
  • ProfileService.connectX accepts the optional profile_created boolean in the response. It's optional so older backends still validate.

startXConnect and disconnectX are exposed on the messenger, and the method action types are regenerated.

References

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Note

Medium Risk
Breaking changes to X connect/createProfile state handling and OAuth entry requirements will affect mobile and other consumers; OAuth unlink uses authenticated DELETE with intentional 404-as-success semantics.

Overview
Adds X OAuth start and unlink APIs on ProfileController/ProfileService, and tightens when X link state is written to controller state.

getXAuthorizationUrl starts the flow: it requires a profile already in state, calls the backend for the authorize URL and state, and returns ephemeral ConnectXSession data without persisting OAuth details in the controller. disconnectX(profileId) calls DELETE profiles/{profileId}/x (404 treated as success) and clears xProfile / connectedToX when the ID matches state.

Breaking behavior changes: connectX now requires a profile in state and, on success, immediately sets xProfile and connectedToX (it no longer relies on createProfile to carry over an x_profile from the create response). getXAuthUrl is bearer-token–scoped on the server with no linkedAddress parameter.

Also fixes the messenger event typing to ProfileController:stateChanged, renames XConnectResponse → ConnectXResponse, and invalidates cached getProfile after connect/disconnect mutations in ProfileService.

Reviewed by Cursor Bugbot for commit 7ec47a5. Bugbot is set up for automated code reviews on this repo. Configure here.

- Add startXConnect to fetch the X authorization URL; profile identity is
  resolved server-side from the bearer token, with an optional linked_address
  for profile auto-creation
- connectX now persists the linked X profile to controller state and returns
  profile_created when the backend auto-creates a profile
- Add disconnectX to clear the linked X profile
- Expose XConnectSession type and regenerate method action types
@montelaidev montelaidev self-assigned this Oct 2, 2026
@montelaidev

Copy link
Copy Markdown
Contributor Author

@metamaskbot publish-previews

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@11.0.1-preview-3b264bf6d
@metamask-previews/accounts-controller@40.0.0-preview-3b264bf6d
@metamask-previews/address-book-controller@8.0.0-preview-3b264bf6d
@metamask-previews/advanced-chart-core@1.0.0-preview-3b264bf6d
@metamask-previews/ai-controllers@2.0.0-preview-3b264bf6d
@metamask-previews/analytics-controller@4.0.0-preview-3b264bf6d
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-3b264bf6d
@metamask-previews/announcement-controller@9.0.0-preview-3b264bf6d
@metamask-previews/app-metadata-controller@3.0.0-preview-3b264bf6d
@metamask-previews/approval-controller@10.0.0-preview-3b264bf6d
@metamask-previews/assets-controller@18.0.1-preview-3b264bf6d
@metamask-previews/assets-controllers@112.1.2-preview-3b264bf6d
@metamask-previews/authenticated-user-storage@4.1.0-preview-3b264bf6d
@metamask-previews/base-controller@10.0.0-preview-3b264bf6d
@metamask-previews/base-data-service@2.1.0-preview-3b264bf6d
@metamask-previews/bitcoin-regtest-up@2.0.0-preview-3b264bf6d
@metamask-previews/bridge-controller@82.0.2-preview-3b264bf6d
@metamask-previews/bridge-status-controller@76.3.5-preview-3b264bf6d
@metamask-previews/build-utils@4.0.0-preview-3b264bf6d
@metamask-previews/chain-agnostic-permission@2.0.0-preview-3b264bf6d
@metamask-previews/chomp-api-service@6.0.1-preview-3b264bf6d
@metamask-previews/claims-controller@1.0.3-preview-3b264bf6d
@metamask-previews/client-controller@2.0.0-preview-3b264bf6d
@metamask-previews/client-utils@3.0.4-preview-3b264bf6d
@metamask-previews/compliance-controller@3.0.0-preview-3b264bf6d
@metamask-previews/composable-controller@13.0.0-preview-3b264bf6d
@metamask-previews/config-registry-controller@5.0.0-preview-3b264bf6d
@metamask-previews/connectivity-controller@1.0.0-preview-3b264bf6d
@metamask-previews/controller-utils@13.0.0-preview-3b264bf6d
@metamask-previews/core-backend@12.0.1-preview-3b264bf6d
@metamask-previews/cryptography@1.1.0-preview-3b264bf6d
@metamask-previews/delegation-controller@4.0.0-preview-3b264bf6d
@metamask-previews/earn-controller@13.0.2-preview-3b264bf6d
@metamask-previews/eip-5792-middleware@4.0.1-preview-3b264bf6d
@metamask-previews/eip-7702-internal-rpc-middleware@1.0.0-preview-3b264bf6d
@metamask-previews/eip1193-permission-middleware@3.0.0-preview-3b264bf6d
@metamask-previews/eth-block-tracker@16.0.0-preview-3b264bf6d
@metamask-previews/eth-json-rpc-middleware@25.0.0-preview-3b264bf6d
@metamask-previews/eth-json-rpc-provider@7.0.0-preview-3b264bf6d
@metamask-previews/foundryup@2.0.0-preview-3b264bf6d
@metamask-previews/gas-fee-controller@27.0.0-preview-3b264bf6d
@metamask-previews/gator-permissions-controller@6.0.1-preview-3b264bf6d
@metamask-previews/geolocation-controller@2.0.0-preview-3b264bf6d
@metamask-previews/java-tron-up@2.0.0-preview-3b264bf6d
@metamask-previews/json-rpc-engine@11.0.0-preview-3b264bf6d
@metamask-previews/json-rpc-middleware-stream@9.0.0-preview-3b264bf6d
@metamask-previews/keyring-controller@28.1.0-preview-3b264bf6d
@metamask-previews/kyc-controller@0.6.1-preview-3b264bf6d
@metamask-previews/local-node-utils@2.0.0-preview-3b264bf6d
@metamask-previews/logging-controller@10.0.0-preview-3b264bf6d
@metamask-previews/message-manager@15.0.0-preview-3b264bf6d
@metamask-previews/messenger@3.0.0-preview-3b264bf6d
@metamask-previews/messenger-cli@1.0.0-preview-3b264bf6d
@metamask-previews/money-account-api-data-service@2.1.0-preview-3b264bf6d
@metamask-previews/money-account-balance-service@3.1.2-preview-3b264bf6d
@metamask-previews/money-account-controller@2.0.0-preview-3b264bf6d
@metamask-previews/money-account-upgrade-controller@5.1.0-preview-3b264bf6d
@metamask-previews/money-account-utils@2.1.0-preview-3b264bf6d
@metamask-previews/multichain-account-service@14.1.0-preview-3b264bf6d
@metamask-previews/multichain-api-middleware@5.0.0-preview-3b264bf6d
@metamask-previews/multichain-network-controller@4.0.0-preview-3b264bf6d
@metamask-previews/multichain-transactions-controller@8.0.0-preview-3b264bf6d
@metamask-previews/name-controller@10.0.0-preview-3b264bf6d
@metamask-previews/network-connection-banner-controller@1.0.0-preview-3b264bf6d
@metamask-previews/network-controller@37.0.1-preview-3b264bf6d
@metamask-previews/network-enablement-controller@7.0.2-preview-3b264bf6d
@metamask-previews/notification-services-controller@29.0.3-preview-3b264bf6d
@metamask-previews/passkey-controller@4.1.0-preview-3b264bf6d
@metamask-previews/permission-controller@14.0.0-preview-3b264bf6d
@metamask-previews/permission-log-controller@6.0.0-preview-3b264bf6d
@metamask-previews/perps-controller@19.0.0-preview-3b264bf6d
@metamask-previews/phishing-controller@18.2.0-preview-3b264bf6d
@metamask-previews/platform-api-docs@0.2.1-preview-3b264bf6d
@metamask-previews/polling-controller@17.0.0-preview-3b264bf6d
@metamask-previews/preferences-controller@24.0.0-preview-3b264bf6d
@metamask-previews/profile-controller@1.0.1-preview-3b264bf6d
@metamask-previews/profile-metrics-controller@5.1.3-preview-3b264bf6d
@metamask-previews/profile-sync-controller@34.0.0-preview-3b264bf6d
@metamask-previews/ramps-controller@26.2.0-preview-3b264bf6d
@metamask-previews/rate-limit-controller@8.0.0-preview-3b264bf6d
@metamask-previews/react-data-query@2.0.0-preview-3b264bf6d
@metamask-previews/remote-feature-flag-controller@7.0.0-preview-3b264bf6d
@metamask-previews/sample-controllers@6.0.0-preview-3b264bf6d
@metamask-previews/seedless-onboarding-controller@11.0.1-preview-3b264bf6d
@metamask-previews/selected-network-controller@27.0.0-preview-3b264bf6d
@metamask-previews/sentinel-api-service@2.0.0-preview-3b264bf6d
@metamask-previews/shield-controller@7.0.4-preview-3b264bf6d
@metamask-previews/signature-controller@40.0.0-preview-3b264bf6d
@metamask-previews/smart-transactions-controller@27.1.0-preview-3b264bf6d
@metamask-previews/snap-account-service@4.0.0-preview-3b264bf6d
@metamask-previews/social-controllers@3.6.0-preview-3b264bf6d
@metamask-previews/solana-test-validator-up@2.0.0-preview-3b264bf6d
@metamask-previews/stellar-quickstart-up@0.0.0-preview-3b264bf6d
@metamask-previews/storage-service@2.0.0-preview-3b264bf6d
@metamask-previews/subscription-controller@12.0.1-preview-3b264bf6d
@metamask-previews/transaction-controller@72.1.0-preview-3b264bf6d
@metamask-previews/transaction-pay-controller@30.0.1-preview-3b264bf6d
@metamask-previews/user-operation-controller@42.0.1-preview-3b264bf6d
@metamask-previews/utils@12.0.0-preview-3b264bf6d
@metamask-previews/wallet@16.0.1-preview-3b264bf6d
@metamask-previews/wallet-cli@0.0.0-preview-3b264bf6d

…ssenger

Use `ControllerStateChangedEvent` instead of the deprecated
`ControllerStateChangeEvent` so the messenger event type matches the
`ProfileController:stateChanged` event that BaseController emits.
@montelaidev
montelaidev marked this pull request as ready for review October 8, 2026 11:56
@montelaidev
montelaidev requested review from a team as code owners October 8, 2026 11:56

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/profile-controller/src/ProfileController.ts Outdated
params,
);
return this.#mapXResponseToXProfile(response);
async connectX(params: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we creating the profile in one step here as well? We had to keep the xprofile and profile row creations step separate because the connect to x and the point that the profile is created is separate in the UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can create as well as link an x profile to a profile.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was saying that we shouldn't create a profile at this point (commented about this here as well: https://github.com/MetaMask/temp-profile-api/pull/2/changes#r4232515587). The UX is multistep so the profile shouldn't be created until createProfile is actually called. We also don't have anything in the designs that would allow a user to call this entrypoint after creating a profile so linking an x profile to an existing profile isn't a real scenario.

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/profile-controller/CHANGELOG.md

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 838633a. Configure here.

Comment thread packages/profile-controller/src/ProfileController.ts Outdated
@montelaidev
montelaidev requested a review from hmalik88 October 9, 2026 16:32
Comment thread packages/profile-controller/src/ProfileController.ts Outdated
Comment thread packages/profile-controller/CHANGELOG.md Outdated
Comment thread packages/profile-controller/src/ProfileController.ts Outdated
mathieuartu
mathieuartu previously approved these changes Oct 12, 2026
@montelaidev
montelaidev enabled auto-merge October 12, 2026 13:13
@montelaidev
montelaidev added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 7063354 Oct 12, 2026
45 checks passed
@montelaidev
montelaidev deleted the feat/x-integration branch October 12, 2026 13:25
Naz-Ovh pushed a commit to 0x-fork/metamask-core that referenced this pull request Oct 12, 2026
…AndUpdateProfile` (MetaMask#10793)

## Explanation

- Recently merged PR MetaMask#10668 added a
`profileId` check in state for x methods that would throw since at the
point that those methods are called, a profile should not exist in
state. Removed that check.
- Added a `fetchAndUpdateProfile` method
- Removed `nullable` from `avatar_url` in the profile api response type

## References

N/A

## Checklist

- [x] I've updated the test suite for new or updated code as appropriate
- [x] I've updated documentation (JSDoc, Markdown, etc.) for new or
updated code as appropriate
- [x] I've communicated my changes to consumers by [updating changelogs
for packages I've
changed](https://github.com/MetaMask/core/tree/main/docs/processes/updating-changelogs.md)
- [x] I've introduced [breaking
changes](https://github.com/MetaMask/core/tree/main/docs/processes/breaking-changes.md)
in this PR and have prepared draft pull requests for clients and
consumer packages to resolve them


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> X OAuth can run without local profile state and `connectedToX` is no
longer updated on connect; `avatar_url` is now required in API
responses, which may fail validation if the API still returns null.
> 
> **Overview**
> **Adds `fetchAndUpdateProfile`** on `ProfileController`, wired through
the messenger to `ProfileService:getProfile`, mapping the API response
into `state.profile` without touching `xProfile`.
> 
> **Relaxes X OAuth entry points** so `getXAuthorizationUrl` and
`connectX` no longer require a profile in local state (removed
`#getProfileIdOrThrow()`). **`connectX` only persists `xProfile`** on
success; it no longer flips `profile.connectedToX` in state.
> 
> **Tightens profile API typing** by making `avatar_url` non-nullable in
`ProfileService` validation and mapping it directly (drops null-to-empty
handling and related tests).
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
2a6e9ee. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants