feat(wallet): Add SeedlessOnboardingController and PasskeyController#9533
feat(wallet): Add SeedlessOnboardingController and PasskeyController#9533lwin-kyaw wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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 d795cf6. Configure here.
|
@metamaskbot publish-preview |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
| messenger, | ||
| state, | ||
| rpId: options.rpId ?? undefined, | ||
| rpName: options.rpName ?? DEFAULT_PASSKEY_RP_NAME, |
There was a problem hiding this comment.
What are your thoughts on having the controller provide these defaults instead of placing them here? Typically we try to keep these initialization files as light as possible. In other words does it make sense for rpName, userName, etc. to be optional at the controller level or it is better for those to be required?
| ...options, | ||
| state, | ||
| messenger, | ||
| encryptor: (options.encryptor ?? |
There was a problem hiding this comment.
Why do we need a typecast here? Is there a way to avoid this?
I think we also do this for KeyringController and I am not sure why. @mikesposito do you know? I think I've asked you this before.
There was a problem hiding this comment.
I'm assuming that the generic types the controller is instantiated with do not match the ones of the encryptor created by the factory. We likely just need to carry additional generic types for encryption key and key derivation options (like KeyringController does IIRC)
There was a problem hiding this comment.
Hmm, okay. Seems like a larger change. I guess we don't have to solve this right now.
| * Encryptor used to protect the seedless onboarding vault. Defaults to a PBKDF2 encryptor | ||
| * configured with 600,000 iterations. | ||
| */ | ||
| encryptor?: GenericEncryptor; |
There was a problem hiding this comment.
Why do we need to override this option just for initialization? (edit: oh, perhaps this is related to #9533 (comment))
| export type PasskeyControllerInstanceOptions = { | ||
| expectedRPID: string | string[]; | ||
|
|
||
| /** | ||
| * Allowed value(s) for the WebAuthn client origin. | ||
| */ | ||
| expectedOrigin: string | string[]; | ||
|
|
||
| /** | ||
| * Relying party ID(s) for verification (SHA-256 hash match in authenticator data). Pass a string or array of strings; an empty array skips RP ID | ||
| * allowlist checks in {@link verifyRegistrationResponse} / {@link verifyAuthenticationResponse}. | ||
| * | ||
| * @default undefined | ||
| */ | ||
| rpId?: string; | ||
|
|
||
| /** | ||
| * Relying party name shown in the platform passkey UI. | ||
| * | ||
| * @default 'MetaMask' | ||
| */ | ||
| rpName?: string; | ||
|
|
||
| /** | ||
| * Optional passkey user name; defaults to `rpName`. | ||
| * | ||
| * @default 'MetaMask Wallet' | ||
| */ | ||
| userName?: string; | ||
|
|
||
| /** | ||
| * Optional display name; defaults to `rpName`. | ||
| * | ||
| * @default 'MetaMask Wallet' | ||
| */ | ||
| userDisplayName?: string; | ||
| }; |
There was a problem hiding this comment.
This is related to #9533 (comment).
If we can make these options optional in the controller, then I'm curious if we can simplify this:
| export type PasskeyControllerInstanceOptions = { | |
| expectedRPID: string | string[]; | |
| /** | |
| * Allowed value(s) for the WebAuthn client origin. | |
| */ | |
| expectedOrigin: string | string[]; | |
| /** | |
| * Relying party ID(s) for verification (SHA-256 hash match in authenticator data). Pass a string or array of strings; an empty array skips RP ID | |
| * allowlist checks in {@link verifyRegistrationResponse} / {@link verifyAuthenticationResponse}. | |
| * | |
| * @default undefined | |
| */ | |
| rpId?: string; | |
| /** | |
| * Relying party name shown in the platform passkey UI. | |
| * | |
| * @default 'MetaMask' | |
| */ | |
| rpName?: string; | |
| /** | |
| * Optional passkey user name; defaults to `rpName`. | |
| * | |
| * @default 'MetaMask Wallet' | |
| */ | |
| userName?: string; | |
| /** | |
| * Optional display name; defaults to `rpName`. | |
| * | |
| * @default 'MetaMask Wallet' | |
| */ | |
| userDisplayName?: string; | |
| }; | |
| export type PasskeyControllerInstanceOptions = Omit< | |
| ConstructorParameters<typeof PasskeyController>[0], | |
| 'messenger' | 'state' | |
| >; |

Explanation
Adds
PasskeyControllerandSeedlessOnboardingControlleras default initialized controllers in thewalletpackage, following theInitializationConfigurationpattern established byKeyringController,AccountsController, and other wallet instances. Each controller gets its own per-directory init module (passkey-controller/,seedless-onboarding-controller/) with colocated types and tests.@metamask/walletis the shared controller-integration layer for extension, mobile, and other clients, so values that differ between clients are injected viainstanceOptionsrather than hardcoded.PasskeyController — WebAuthn relying-party configuration is platform-specific, so
passkeyController.expectedRPIDandpasskeyController.expectedOriginmust be supplied by each client (see the options table below).rpName,userName, anduserDisplayNamedefault to MetaMask values when omitted ('MetaMask','MetaMask Wallet','MetaMask Wallet'). Persisted passkey state is forwarded from wallet state when present.SeedlessOnboardingController — JWT lifecycle callbacks (
refreshJWTToken,revokeRefreshToken,renewRefreshToken) are platform-specific and must be injected by each client (typically delegating toOAuthServiceon extension orAuthTokenHandleron mobile). A default PBKDF2 encryptor (600,000 iterations, shared withKeyringController) is applied when no customencryptoris passed. All otherSeedlessOnboardingControllerconstructor options (e.g.network,passwordOutdatedCacheTTL,keyDerivationInterface) are forwarded frominstanceOptionsas-is.Neither controller requires messenger delegation beyond the standard
getState/stateChangedwiring — both are self-contained at the messenger level.References
Per-environment options
passkeyController.expectedRPIDextension.runtime.getURL('')(stripped trailing slash)'extension-id')passkeyController.expectedOriginexpectedRPID'https://extension.origin')passkeyController.rpName/userName/userDisplayName'MetaMask'/'MetaMask Wallet'/'MetaMask Wallet'(wallet defaults match current extension values)seedlessOnboardingController.refreshJWTTokenOAuthService:getNewRefreshTokenvia init messengerAuthTokenHandlerjest.fn()in unit testsseedlessOnboardingController.revokeRefreshTokenOAuthService:revokeRefreshTokenAuthTokenHandlerjest.fn()in unit testsseedlessOnboardingController.renewRefreshTokenOAuthService:renewRefreshTokenAuthTokenHandlerjest.fn()in unit testsseedlessOnboardingController.encryptorencryptorFactory(600_000)(extension-local factory)Encryptoradapter (cipher↔datanormalization)encryptorFactory(600_000)seedlessOnboardingController.networkWeb3AuthNetwork.Devnet(dev/test) orMainnet(prod)web3AuthNetworkconstantPasskeyControllerhas no messenger-level dependencies on other controllers.SeedlessOnboardingControllercurrently receives JWT callbacks out-of-band (not via messenger); a future refactor may route these throughOAuthServiceactions directly.Client construction sites
These are the current
mainconstruction sites the compatibility audit was based on (the adoption PRs below will replace them):PasskeyController
SeedlessOnboardingController
Client adoption PRs
Checklist
Note
High Risk
Marked breaking default wallet initialization for passkey and seedless onboarding (WebAuthn, JWT, vault encryption), so extension/mobile consumers must adopt new instance options and dependency bumps.
Overview
Breaking: The
@metamask/walletpackage now includes PasskeyController and SeedlessOnboardingController in its default controller set, with new dependencies and TypeScript project references.New initialization modules follow the existing instance pattern: messengers,
initwiring, and optionalinstanceOptionsonWalletOptions(passkeyController,seedlessOnboardingController). Passkey init requiresexpectedRPIDandexpectedOrigin, applies MetaMask defaults for RP/user display fields when omitted, and forwards persisted passkey state. Seedless onboarding forwards JWT lifecycle callbacks from options and supplies a default PBKDF2 encryptor (600k iterations, shared with keyring) unless a custom encryptor is passed.CODEOWNERS assigns the new init paths to
@MetaMask/web3auth; README’s dependency graph and wallet changelog document the change. Unit tests cover registration indefaultConfigurations, option forwarding, and messenger state access.Reviewed by Cursor Bugbot for commit b867f45. Bugbot is set up for automated code reviews on this repo. Configure here.