Skip to content

3.0.5#402

Merged
Dargon789 merged 2 commits intoDargon789:masterfrom
0xsequence:master
Mar 26, 2026
Merged

3.0.5#402
Dargon789 merged 2 commits intoDargon789:masterfrom
0xsequence:master

Conversation

@Dargon789
Copy link
Copy Markdown
Owner

@Dargon789 Dargon789 commented Mar 21, 2026

Summary by Sourcery

Add account federation capabilities and extend OAuth auth code flows to support signup, reauth, and add-signer flows while bumping package versions to 3.0.5.

New Features:

  • Allow adding additional login signers to existing wallets, including via redirect-based OAuth flows, and completing those configuration updates.
  • Allow removing login signers from wallets with safeguards to prevent removing the last remaining login signer.

Enhancements:

  • Generalize auth code commitment handling from a boolean signup flag to typed auth, reauth, and add-signer modes, including PKCE, and propagate this through handlers and wallet redirect completion logic.

Build:

  • Bump versions of wallet, services, and utility packages to 3.0.5 and align internal dependency versions for account federation support.

Documentation:

  • Document the new account federation APIs and flows in the wallet WDK changelog and public type exports.

Tests:

  • Update and extend auth code, PKCE, identity DB, and wallet tests to cover the new auth commitment types and add-signer/reauth flows.

patrislav and others added 2 commits March 19, 2026 18:03
* wdk: account federation

* wdk: address review comments; refactor auth commitments

* wdk: update tests

* fix test
@codesandbox
Copy link
Copy Markdown

codesandbox bot commented Mar 21, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 21, 2026

Reviewer's Guide

Implements account federation for wallets by adding add/remove login signer APIs and redirect flows, refactoring auth commitments from a boolean isSignUp flag to a discriminated union of auth/reauth/add-signer, and wiring configuration updates, witnessing, and tests to support these new flows, along with version/changelog bumps to 3.0.5 across packages.

Class diagram for Wallets account federation and auth commitment types

classDiagram
  class WalletsInterface {
    <<interface>>
    +addLoginSigner(args AddLoginSignerArgs) Promise~string~
    +completeAddLoginSigner(requestId string) Promise~void~
    +startAddLoginSignerWithRedirect(args StartAddLoginSignerWithRedirectArgs) Promise~string~
    +removeLoginSigner(args RemoveLoginSignerArgs) Promise~string~
    +completeRemoveLoginSigner(requestId string) Promise~void~
  }

  class Wallets {
    +addLoginSigner(args AddLoginSignerArgs) Promise~string~
    +completeAddLoginSigner(requestId string) Promise~void~
    +startAddLoginSignerWithRedirect(args StartAddLoginSignerWithRedirectArgs) Promise~string~
    +removeLoginSigner(args RemoveLoginSignerArgs) Promise~string~
    +completeRemoveLoginSigner(requestId string) Promise~void~
    -addLoginSignerFromPrepared(wallet Address, loginSigner LoginSignerPrepared) Promise~string~
  }

  class StartAddLoginSignerWithRedirectArgs {
    +wallet Address
    +kind AuthKind
    +target string
  }

  class AddLoginSignerArgs {
    +wallet Address
  }

  class AddLoginSignerArgsMnemonic {
    +kind "mnemonic"
    +mnemonic string
  }

  class AddLoginSignerArgsEmailOtp {
    +kind "email-otp"
    +email string
  }

  class AddLoginSignerArgsIdToken {
    +kind IdTokenKind
    +idToken string
  }

  class RemoveLoginSignerArgs {
    +wallet Address
    +signerAddress Address
  }

  class LoginSignerPrepared {
    +signer SignerWitnessable
    +extra WitnessExtraSignerKind
  }

  class AuthCommitments {
    +set(commitment AuthCommitment) Promise~void~
    +get(id string) Promise~AuthCommitment~
  }

  class AuthCommitment {
    +id string
    +kind AuthKind
    +verifier string
    +challenge string
    +target string
  }

  class AuthCommitmentAuth {
    +type "auth"
  }

  class AuthCommitmentReauth {
    +type "reauth"
    +signer string
  }

  class AuthCommitmentAddSigner {
    +type "add-signer"
    +wallet string
  }

  class CommitAuthArgsAuth {
    +type "auth"
    +state string
  }

  class CommitAuthArgsReauth {
    +type "reauth"
    +state string
    +signer string
  }

  class CommitAuthArgsAddSigner {
    +type "add-signer"
    +state string
    +wallet string
  }

  class AuthCodeHandler {
    +commitAuth(target string, args CommitAuthArgs) Promise~string~
    +completeAuth(commitment AuthCommitment, code string) Promise~IdentitySignerMetadata~
  }

  class AuthCodePkceHandler {
    +commitAuth(target string, args CommitAuthArgs) Promise~string~
  }

  class ActionsEnum {
    <<enumeration>>
    +AddLoginSigner
    +RemoveLoginSigner
  }

  class SignaturesModule {
    +get(id string) SignatureRequest
  }

  class SignatureRequest {
    +action ActionsEnum
  }

  class AuthKind {
    <<type>>
    +values google-pkce, apple, custom-*
  }

  class IdTokenKind {
    <<type>>
    +values google-id-token, apple-id-token, custom-*
  }

  class SignerWitnessable {
    +address Address
    +witness(stateProvider any, wallet Address, extra WitnessExtraSignerKind) Promise~void~
  }

  class WitnessExtraSignerKind {
  }

  WalletsInterface <|.. Wallets

  AddLoginSignerArgs o-- AddLoginSignerArgsMnemonic
  AddLoginSignerArgs o-- AddLoginSignerArgsEmailOtp
  AddLoginSignerArgs o-- AddLoginSignerArgsIdToken

  AuthCommitment <|-- AuthCommitmentAuth
  AuthCommitment <|-- AuthCommitmentReauth
  AuthCommitment <|-- AuthCommitmentAddSigner

  CommitAuthArgsAuth <|-- CommitAuthArgsReauth
  CommitAuthArgsAuth <|-- CommitAuthArgsAddSigner

  AuthCodeHandler <|-- AuthCodePkceHandler

  Wallets --> AuthCommitments
  Wallets --> AuthCodeHandler
  Wallets --> SignaturesModule
  AuthCodeHandler --> AuthCommitments
  AuthCodePkceHandler --> AuthCommitments

  SignatureRequest --> ActionsEnum
  AuthCommitment --> AuthKind
  AddLoginSignerArgsIdToken --> IdTokenKind
Loading

File-Level Changes

Change Details Files
Add wallet login-signer federation APIs (add/remove, including redirect-based social flows) and underlying config update wiring.
  • Introduce StartAddLoginSignerWithRedirectArgs, AddLoginSignerArgs, and RemoveLoginSignerArgs types and export them from the public sequence index
  • Extend WalletsInterface with addLoginSigner, completeAddLoginSigner, removeLoginSigner, completeRemoveLoginSigner, and startAddLoginSignerWithRedirect methods, including detailed JSDoc
  • Implement addLoginSigner to prepare a new login signer via existing signup pipeline and delegate to addLoginSignerFromPrepared
  • Implement completeAddLoginSigner and completeRemoveLoginSigner to validate signature request actions and call completeConfigurationUpdate
  • Implement removeLoginSigner to rebuild login topology without the removed signer, enforce at least one remaining login signer, update sessions/recovery modules, and request a configuration update
  • Add private helper addLoginSignerFromPrepared to update login topology, sessions/recovery modules, witness the new signer for discovery, and create an AddLoginSigner configuration update request
  • Update completeRedirect to branch on commitment.type (add-signer/auth/reauth), and for add-signer complete auth, derive signer kind, and call addLoginSignerFromPrepared
  • Update startSignUpWithRedirect and new startAddLoginSignerWithRedirect to call AuthCodeHandler/AuthCodePkceHandler.commitAuth with structured type arguments (auth vs add-signer) instead of boolean isSignUp
packages/wallet/wdk/src/sequence/wallets.ts
packages/wallet/wdk/src/sequence/index.ts
packages/wallet/wdk/src/sequence/types/signature-request.ts
packages/wallet/wdk/test/wallets.test.ts
Refactor auth commitments and auth-code handlers from an isSignUp boolean to a discriminated union of auth, reauth, and add-signer, and propagate through PKCE and DB layers.
  • Define CommitAuthArgs union (auth, reauth, add-signer) and use it in AuthCodeHandler/AuthCodePkceHandler.commitAuth signatures
  • Change AuthCommitment type from an isSignUp + optional signer field to a union over type: 'auth'
'reauth'
Wire new login-signer actions into the signature request model and update package metadata for the 3.0.5 release.
  • Extend ActionToPayload and Actions enums with AddLoginSigner and RemoveLoginSigner mapped to configuration-update payloads
  • Add 3.0.5 sections to various CHANGELOGs describing account-federation support and dependency bumps
  • Bump version fields from 3.0.4 to 3.0.5 across affected package.json files (services, wallet core, wdk, dapp-client, primitives, utils/abi, relayer, etc.)
packages/wallet/wdk/src/sequence/types/signature-request.ts
packages/wallet/wdk/CHANGELOG.md
packages/wallet/dapp-client/CHANGELOG.md
packages/wallet/core/CHANGELOG.md
packages/services/relayer/CHANGELOG.md
packages/services/api/CHANGELOG.md
packages/services/builder/CHANGELOG.md
packages/services/guard/CHANGELOG.md
packages/services/identity-instrument/CHANGELOG.md
packages/services/indexer/CHANGELOG.md
packages/services/marketplace/CHANGELOG.md
packages/services/metadata/CHANGELOG.md
packages/services/userdata/CHANGELOG.md
packages/utils/abi/CHANGELOG.md
packages/wallet/primitives/CHANGELOG.md
packages/services/api/package.json
packages/services/builder/package.json
packages/services/guard/package.json
packages/services/identity-instrument/package.json
packages/services/indexer/package.json
packages/services/marketplace/package.json
packages/services/metadata/package.json
packages/services/relayer/package.json
packages/services/userdata/package.json
packages/utils/abi/package.json
packages/wallet/core/package.json
packages/wallet/dapp-client/package.json
packages/wallet/primitives/package.json
packages/wallet/wdk/package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 21, 2026

@taylanpince is attempting to deploy a commit to the Foundry development Team on Vercel.

A member of the Team first needs to authorize it.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces significant enhancements to account management by implementing account federation support. This feature allows users to link various login methods to a single wallet, improving flexibility and user experience. The changes involve a version bump across several packages, a refactoring of authentication commitment types, and the addition of new wallet interface methods and signature request actions to facilitate the management of these federated login signers.

Highlights

  • Version Bump: Multiple @0xsequence packages have been updated to version 3.0.5.
  • Account Federation Support: Introduced new functionality to allow users to link multiple login methods (e.g., social logins, mnemonics) to a single wallet, enabling flexible account access and discovery.
  • Refactored Authentication Commitments: The AuthCommitment type was refactored to use a discriminated union, explicitly defining authentication flow types as auth, reauth, or add-signer, replacing the previous isSignUp boolean and optional signer field.
  • New Wallet Interface Methods: Added new methods to the Wallets interface, including addLoginSigner, completeAddLoginSigner, startAddLoginSignerWithRedirect, removeLoginSigner, and completeRemoveLoginSigner, to manage the lifecycle of federated login signers.
  • Extended Signature Request Actions: New actions, AddLoginSigner and RemoveLoginSigner, were added to the SignatureRequest types to support configuration updates related to account federation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@snyk-io
Copy link
Copy Markdown

snyk-io bot commented Mar 21, 2026

Snyk checks have failed. 3 issues have been found so far.

Status Scan Engine Critical High Medium Low Total (3)
Open Source Security 0 1 2 0 3 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@mergify
Copy link
Copy Markdown

mergify bot commented Mar 21, 2026

⚠️ The sha of the head commit of this PR conflicts with #321. Mergify cannot evaluate rules on this PR. Once #321 is merged or closed, Mergify will resume processing this PR. ⚠️

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 5 issues, and left some high level feedback:

  • The change from isSignUp/signer? to a discriminated type union on AuthCommitment means existing DB entries created by older versions will not have type set and will hit the switch (commitment.type) in completeRedirect without a matching case—consider adding a backward‑compatibility path (e.g. inferring type from legacy fields) or a migration/guard that handles legacy commitments explicitly.
  • In addLoginSignerFromPrepared, you call witness on the new signer before the configuration update is signed and applied; this may let the account become discoverable via a credential that is not yet part of the wallet config if the update is rejected or fails—consider deferring the witness call until after completeConfigurationUpdate, or otherwise making the two steps atomic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The change from `isSignUp`/`signer?` to a discriminated `type` union on `AuthCommitment` means existing DB entries created by older versions will not have `type` set and will hit the `switch (commitment.type)` in `completeRedirect` without a matching case—consider adding a backward‑compatibility path (e.g. inferring `type` from legacy fields) or a migration/guard that handles legacy commitments explicitly.
- In `addLoginSignerFromPrepared`, you call `witness` on the new signer before the configuration update is signed and applied; this may let the account become discoverable via a credential that is not yet part of the wallet config if the update is rejected or fails—consider deferring the witness call until after `completeConfigurationUpdate`, or otherwise making the two steps atomic.

## Individual Comments

### Comment 1
<location path="packages/wallet/wdk/src/sequence/wallets.ts" line_range="1457-1459" />
<code_context>
+    const { loginTopology, modules } = await this.getConfigurationParts(args.wallet)
+
+    const existingSigners = Config.getSigners(loginTopology)
+    const allExistingAddresses = [...existingSigners.signers, ...existingSigners.sapientSigners.map((s) => s.address)]
+
+    if (!allExistingAddresses.some((addr) => Address.isEqual(addr, args.signerAddress))) {
+      throw new Error('signer-not-found')
+    }
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid allowing removal of the ZeroAddress sentinel as a login signer.

In `removeLoginSigner`, `allExistingAddresses` includes `Constants.ZeroAddress`, so a call with `signerAddress = ZeroAddress` will pass the `signer-not-found` check, even though you later filter `ZeroAddress` out of `remainingMembers`. Since the rest of the code treats `ZeroAddress` as a sentinel, it shouldn’t be removable. Consider either excluding `ZeroAddress` from `allExistingAddresses` in the presence check or explicitly rejecting attempts to remove it with a dedicated error to keep the sentinel from being treated as a normal signer.
</issue_to_address>

### Comment 2
<location path="packages/wallet/wdk/test/authcode-pkce.test.ts" line_range="91" />
<code_context>
   describe('commitAuth', () => {
     it('Should create Google PKCE auth commitment and return OAuth URL', async () => {
       const target = 'https://example.com/success'
-      const isSignUp = true
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding explicit tests that assert on the new `type` discriminator for all flows, including add-signer

The updated tests now assert `type: 'auth'` / `type: 'reauth'` on commitments, which is good, but the new `add-signer` flow isn’t covered, and some tests still emphasize URL/`metadata` instead of the discriminated union shape.

Please add:
- A test for `commitAuth(target, { type: 'add-signer', wallet: '0x...' })` that checks the stored commitment includes `type: 'add-signer'` and the correct `wallet`.
- Optionally, a (parameterized) test that verifies the `type` field for all three modes (`auth`, `reauth`, `add-signer`) to protect the `CommitAuthArgs` union behavior from regressions in the PKCE handler.

Suggested implementation:

```typescript
  describe('commitAuth', () => {
    it('Should create Google PKCE auth commitment and return OAuth URL', async () => {
      const target = 'https://example.com/success'

      const result = await handler.commitAuth(target, { type: 'auth' })

      // Verify nitroCommitVerifier was called with correct challenge and auth type
      expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(
        expect.objectContaining({
          challenge: 'mock-challenge-hash',
          target,
          metadata: expect.objectContaining({
            type: 'auth',
          }),
        }),
      )

      // Keep existing expectations about the returned OAuth URL, verifier options, etc.
      // e.g. expect(result).toBe(expectedUrl)
      //      expect(...).toHaveBeenCalledWith(...)
    })

    it('Should create add-signer PKCE auth commitment with correct type and wallet', async () => {
      const target = 'https://example.com/success'
      const wallet = '0x1234567890abcdef1234567890abcdef12345678'

      await handler.commitAuth(target, { type: 'add-signer', wallet })

      expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(
        expect.objectContaining({
          challenge: 'mock-challenge-hash',
          target,
          metadata: expect.objectContaining({
            type: 'add-signer',
            wallet,
          }),
        }),
      )
    })

    it.each([
      { label: 'auth', mode: { type: 'auth' as const }, expectedType: 'auth' },
      {
        label: 'reauth',
        mode: { type: 'reauth' as const },
        expectedType: 'reauth',
      },
      {
        label: 'add-signer',
        mode: {
          type: 'add-signer' as const,
          wallet: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
        },
        expectedType: 'add-signer',
      },
    ])(
      'Should store commitment with correct type discriminator for %s flow',
      async ({ mode, expectedType }) => {
        const target = 'https://example.com/success'

        await handler.commitAuth(target, mode as any)

        expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(
          expect.objectContaining({
            metadata: expect.objectContaining({
              type: expectedType,
            }),
          }),
        )
      },
    )

```

Depending on the actual implementation, you may need to:

1. Adjust the `expect(handler['nitroCommitVerifier']).toHaveBeenCalledWith(...)` shape if `nitroCommitVerifier` does not receive a single object argument. If it receives positional arguments, update the expectations to match your real signature, e.g.:
   - `toHaveBeenCalledWith('mock-challenge-hash', target, expect.objectContaining({ type: 'auth' }))`.
2. Ensure the `metadata` structure truly contains `{ type, wallet? }`. If `type` is stored at a different level, change `metadata: expect.objectContaining({ type: ... })` to match the real data path.
3. If the type of `commitAuth` does not accept `{ type: 'reauth' }` or `{ type: 'add-signer', wallet }` exactly as written, update the `mode` objects in `it.each` to conform to your existing `CommitAuthArgs` union type.
4. If you already have separate tests for `reauth`, you can merge or deduplicate them so that you don’t have overlapping coverage with conflicting expectations.
</issue_to_address>

### Comment 3
<location path="packages/wallet/wdk/test/authcode.test.ts" line_range="248" />
<code_context>

     it('Should create auth commitment and return OAuth URL', async () => {
       const target = '/test-target'
-      const isSignUp = true
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for `type: 'add-signer'` commitments and ensure all `CommitAuthArgs` variants are tested

The tests now cover `type: 'auth'` and `type: 'reauth'` in `AuthCodeHandler.commitAuth`, but not the new `type: 'add-signer'` variant.

Please add a test that calls `commitAuth('/target', { type: 'add-signer', wallet: testWallet })` and asserts that `mockAuthCommitmentsSet` receives a commitment with `type: 'add-signer'` and `wallet: testWallet`.

You could also add a small table-driven test over all three `type` values to verify the expected presence/absence of `signer` and `wallet` fields, matching the discriminated union definition.
</issue_to_address>

### Comment 4
<location path="packages/wallet/wdk/test/identity-auth-dbs.test.ts" line_range="90-85" />
<code_context>
 {
   "name": "@0xsequence/api",
-  "version": "3.0.4",
</code_context>
<issue_to_address>
**suggestion (testing):** AuthCommitments DB tests should validate the new `add-signer` commitment variant

Tests currently cover only the `auth` and `reauth` variants of `AuthCommitment`, but not `add-signer`. Please add a test that writes an `AuthCommitment` with `type: 'add-signer'` and a `wallet` value, then reads it back via `get` and asserts both the `type` and `wallet` fields are preserved. This will confirm the DB schema and serialization handle the new variant correctly.
</issue_to_address>

### Comment 5
<location path="packages/wallet/wdk/src/sequence/wallets.ts" line_range="926" />
<code_context>
+    return handler.commitAuth(args.target, { type: 'add-signer', wallet: args.wallet })
   }

   async completeRedirect(args: CompleteRedirectArgs): Promise<string> {
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting shared helpers for handler lookup, topology computation, and module updates to simplify redirect flows and login-signer add/remove logic.

You can reduce complexity without losing any behavior by factoring out some repeated logic and isolating pure computations.

### 1. Reduce duplication in `completeRedirect` (and redirect starters)

The handler lookup + validation is now repeated in multiple places:

- `startSignUpWithRedirect`
- `startAddLoginSignerWithRedirect`
- `completeRedirect` (`add-signer` and `reauth` cases)

Extract a small helper and reuse it:

```ts
private getAuthCodeHandlerForKind(kind: SignupArgs['kind'] | AuthCommitment['kind']): AuthCodeHandler {
  const handlerKind = getSignupHandlerKey(kind)
  const handler = this.shared.handlers.get(handlerKind)
  if (!handler) throw new Error('handler-not-registered')
  if (!(handler instanceof AuthCodeHandler)) throw new Error('handler-does-not-support-redirect')
  return handler
}
```

Then the call sites become much simpler:

```ts
async startSignUpWithRedirect(args: StartSignUpWithRedirectArgs) {
  const handler = this.getAuthCodeHandlerForKind(args.kind)
  return handler.commitAuth(args.target, { type: 'auth' })
}

async startAddLoginSignerWithRedirect(args: StartAddLoginSignerWithRedirectArgs) {
  const walletEntry = await this.get(args.wallet)
  if (!walletEntry) throw new Error('wallet-not-found')
  if (walletEntry.status !== 'ready') throw new Error('wallet-not-ready')

  const handler = this.getAuthCodeHandlerForKind(args.kind)
  return handler.commitAuth(args.target, { type: 'add-signer', wallet: args.wallet })
}
```

And `completeRedirect` becomes more readable:

```ts
async completeRedirect(args: CompleteRedirectArgs): Promise<string> {
  const commitment = await this.shared.databases.authCommitments.get(args.state)
  if (!commitment) throw new Error('invalid-state')

  switch (commitment.type) {
    case 'add-signer': {
      const handler = this.getAuthCodeHandlerForKind(commitment.kind)

      const walletAddress = commitment.wallet as Address.Address
      const walletEntry = await this.get(walletAddress)
      if (!walletEntry) throw new Error('wallet-not-found')
      if (walletEntry.status !== 'ready') throw new Error('wallet-not-ready')

      const [signer] = await handler.completeAuth(commitment, args.code)
      const signerKind = getSignerKindForSignup(commitment.kind)

      await this.addLoginSignerFromPrepared(walletAddress, { signer, extra: { signerKind } })
      break
    }

    case 'auth': {
      await this.signUp({
        kind: commitment.kind,
        commitment,
        code: args.code,
        noGuard: args.noGuard,
        target: commitment.target,
        isRedirect: true,
        use4337: args.use4337,
      })
      break
    }

    case 'reauth': {
      const handler = this.getAuthCodeHandlerForKind(commitment.kind)
      await handler.completeAuth(commitment, args.code)
      break
    }
  }

  if (!commitment.target) throw new Error('invalid-state')
  return commitment.target
}
```

If you want to go one step further, you can also split each switch branch into private methods (`handleAddSignerRedirect`, `handleAuthRedirect`, `handleReauthRedirect`) to keep `completeRedirect` very shallow.

---

### 2. Split responsibilities in `addLoginSignerFromPrepared` (and share with `removeLoginSigner`)

`addLoginSignerFromPrepared` currently does all of the following:

- Reads configuration
- Computes next login topology
- Mutates session and recovery modules
- Performs the witness
- Requests configuration update

You can move the pure topology computation to a helper that can also be reused in `removeLoginSigner`:

```ts
private computeNextLoginTopologyWithAddedSigner(
  loginTopology: Config.Topology,
  newMember: { address: Address.Address; imageHash?: Hex.Hex },
): Config.Topology {
  const existingSigners = Config.getSigners(loginTopology)
  const existingMembers = [
    ...existingSigners.signers
      .filter((x) => x !== Constants.ZeroAddress)
      .map((x) => ({ address: x })),
    ...existingSigners.sapientSigners.map((x) => ({ address: x.address, imageHash: x.imageHash })),
  ]

  return buildCappedTree([...existingMembers, newMember])
}
```

Then `addLoginSignerFromPrepared` reads more linearly:

```ts
private async addLoginSignerFromPrepared(
  wallet: Address.Address,
  loginSigner: {
    signer: (Signers.Signer | Signers.SapientSigner) & Signers.Witnessable
    extra: WitnessExtraSignerKind
  },
): Promise<string> {
  const newSignerAddress = await loginSigner.signer.address
  const { loginTopology, modules } = await this.getConfigurationParts(wallet)

  const existingSigners = Config.getSigners(loginTopology)
  const allExistingAddresses = [
    ...existingSigners.signers,
    ...existingSigners.sapientSigners.map((s) => s.address),
  ]
  if (allExistingAddresses.some((addr) => Address.isEqual(addr, newSignerAddress))) {
    throw new Error('signer-already-exists')
  }

  const newMember = {
    address: newSignerAddress,
    imageHash: Signers.isSapientSigner(loginSigner.signer)
      ? await loginSigner.signer.imageHash
      : undefined,
  }
  const nextLoginTopology = this.computeNextLoginTopologyWithAddedSigner(loginTopology, newMember)

  await this.addLoginSignerToModules(modules, newSignerAddress, loginSigner.signer)
  await loginSigner.signer.witness(this.shared.sequence.stateProvider, wallet, loginSigner.extra)

  return this.requestConfigurationUpdate(
    wallet,
    { loginTopology: nextLoginTopology, modules },
    Actions.AddLoginSigner,
    'wallet-webapp',
  )
}
```

With a small helper for module updates:

```ts
private async addLoginSignerToModules(
  modules: Module[],
  signerAddress: Address.Address,
  signer: Signers.Signer | Signers.SapientSigner,
): Promise<void> {
  if (!Signers.isSapientSigner(signer) && this.shared.modules.sessions.hasSessionModule(modules)) {
    await this.shared.modules.sessions.addIdentitySignerToModules(modules, signerAddress)
  }

  if (this.shared.modules.recovery.hasRecoveryModule(modules)) {
    await this.shared.modules.recovery.addRecoverySignerToModules(modules, signerAddress)
  }
}
```

Similarly, `removeLoginSigner`’s topology update and module updates could reuse complementary helpers (`computeNextLoginTopologyWithRemovedSigner`, `removeLoginSignerFromModules`), which would make both add/remove flows easier to follow and maintain:

```ts
private computeNextLoginTopologyWithRemovedSigner(
  loginTopology: Config.Topology,
  signerToRemove: Address.Address,
): Config.Topology {
  const existingSigners = Config.getSigners(loginTopology)
  const remainingMembers = [
    ...existingSigners.signers
      .filter((x) => x !== Constants.ZeroAddress && !Address.isEqual(x, signerToRemove))
      .map((x) => ({ address: x })),
    ...existingSigners.sapientSigners
      .filter((x) => !Address.isEqual(x.address, signerToRemove))
      .map((x) => ({ address: x.address, imageHash: x.imageHash })),
  ]
  if (remainingMembers.length < 1) {
    throw new Error('cannot-remove-last-login-signer')
  }
  return buildCappedTree(remainingMembers)
}
```

Using these small helpers keeps the imperative methods focused on the high-level flow (“validate wallet”, “compute next topology”, “update modules”, “witness”, “request config update”) and reduces cognitive load without changing any behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces account federation capabilities, which is a significant and valuable feature. The refactoring of AuthCommitment from using a boolean isSignUp flag to a discriminated union with type is a great improvement for type safety and code clarity. The new methods for adding and removing login signers are well-implemented and follow the existing patterns in the codebase. The tests have also been updated to cover the new functionality. I've left a couple of minor suggestions to reduce code duplication, but overall this is a solid contribution.

@Dargon789 Dargon789 merged commit e3347ec into Dargon789:master Mar 26, 2026
3 of 11 checks passed
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