Skip to content

release: to prod#1383

Merged
joelorzet merged 8 commits into
prodfrom
staging
May 26, 2026
Merged

release: to prod#1383
joelorzet merged 8 commits into
prodfrom
staging

Conversation

@joelorzet

Copy link
Copy Markdown

No description provided.

joelorzet added 8 commits May 26, 2026 10:15
The enrollment wizard previously required users to click Copy AND
Download AND retype two codes at random positions before letting
them past the save-codes step. KEEP-640 #4 calls that out as too
heavy and asks for Download as the sole gate.

  - Backup-codes panel: drop the retype-at-position confirmation
    block and the codes-hidden state. Render codes throughout the
    panel; expose Copy as an optional ghost button; gate the
    "I've saved my codes" CTA on Download alone.
  - Setup dialog: update the lock-error toasts so they ask the
    user to download (not "copy and confirm two") before closing.

Lock-on-close stays intact: the dialog still refuses to close
while codes are visible and the user has not confirmed.
The DualFactorSteps wizard renders a Cancel button on the email
phase that fires the consumer's onBack prop. Most consumers wired
that to "step back one phase inside the same modal" (setState,
setPhase, setMode), so Cancel from inside the MFA prompt dropped
the user back at the input form instead of closing the modal.
KEEP-640 #1 calls that out as broken.

Switch each consumer's onBack to fully close its wrapping modal:

  - withdraw-modal: closeAll() instead of setState("input"). The
    user aborts the whole withdrawal flow rather than re-editing
    amount/recipient inside an MFA prompt they just bailed on.
  - api-keys-overlay (create-key path): pop() instead of
    setPhase("label"). Same shape as the existing pop-on-Cancel
    on the export-private-key flow.
  - delete-account-section: setOpen(false) + resetAll() instead
    of setPhase("confirmation"). Closes the AlertDialog entirely.
  - totp-manage-dialog (disable flow): onOpenChange(false) so
    Cancel aborts the disable, rather than dropping back to the
    summary listing where a second Cancel is needed to leave.

The locked enroll/verify gate pages (/enroll-mfa, /verify-mfa)
keep their hard-locked behavior; the user has no session to fall
back to in those cases, so Cancel cannot be a no-op there.

The change-password-section panel stays on setPhase("passwords")
because it is an inline section on the settings page, not a
modal -- Cancel there is "back to the password form", which is
the expected shape for an inline wizard.
When a user has more than one OAuth provider linked (Google +
GitHub) and they try to reset a password they do not have, the
"this account uses social login" reminder previously named
whichever provider appeared first in OAUTH_PROVIDERS. With the
constant ordered ["github", "google"], a Google user who had
also linked GitHub would receive an email saying "your account
uses GitHub". KEEP-640 #7.

Sort the linked OAuth accounts by accounts.updated_at DESC and
pick the most recently used. Better Auth bumps updated_at on
every successful sign-in via that provider, so this matches the
provider the user just attempted to use.
Forward port of the same fix in PR #1367. The
session.create.before hook in lib/auth.ts stamps
requires_mfa = true on every fresh session of a user with
two_factor_enabled. That gate is for the legacy single-factor
sign-in path; strict-signin verifies password + email OTP + TOTP
atomically before the session is minted, so the freshly minted
session is already fully MFA-verified.

Without this clear, the proxy gate sees requires_mfa = true on
the new session right after sign-in and 302s the user to
/verify-mfa, where they re-enter both codes for nothing. KEEP-640 #5.

Same shape as /api/user/totp/enroll: UPDATE sessions SET
requires_mfa = false, mfa_verified_at = now() scoped by user_id.
If Better Auth's OAuth callback returns a redirect without the
session cookie we expect, interceptOauthCallback silently bails
and returns the response untouched. That path is implicated in
the "signed in with Google but no session on KeeperHub" reports
that triggered KEEP-640 #2/#3. Add a single warn log so we can
tell next time whether the Set-Cookie array was empty, missing
the session_token name, or the runtime did not expose
getSetCookie.

Diagnostic only; behaviour unchanged.
Iterating on KEEP-640 #4 with feedback during testing. The wizard
was three sequential phases (Scan -> Verify -> Save codes); fold
the first two into a single "Scan & verify" step with the QR,
setup-key chip, and TOTP input all on one screen, and rename the
second to "Download codes". Step 2 is now opt-in: downloading is
not required to close the dialog. A "Skip for now" button on the
parent footer is the only thing that closes step 2; the codes can
be regenerated from settings later.

  - totp-setup-dialog.tsx:
      - Phase list shrinks from three to two; STEP_DEFS reflects the
        new labels.
      - Setup-key row is now a single line: label + clickable chip
        with the manual-entry key in monospace + ghost copy button.
        Clicking the chip or the button calls handleCopyKey; a
        3-second keyJustCopied flag flashes the chip border emerald
        so it's clear which control fired. Both chip and button are
        h-7 so they sit flush.
      - The "verify" phase merges into the setup phase: the TOTP
        input renders below the setup key on the same screen, and
        Continue calls /enroll directly.
      - Step 2 strips the redundant alert + verbose description; one
        short sentence covers what the codes are for.
  - totp-backup-codes-panel.tsx:
      - Drop the now-redundant onConfirmed prop. Panel is purely a
        display + Copy + Download component; closing the dialog is
        the parent's responsibility via an explicit Skip / Done.
      - Drop the Alert that duplicated the parent's description.
  - totp-manage-dialog.tsx:
      - The viewing-codes mode (regenerate flow) now gets its own
        Done button in a DialogFooter, since the panel no longer
        emits onConfirmed.

No backend changes: /api/user/totp/setup still issues TOTP secret +
manual entry key, /api/user/totp/enroll still verifies the code and
returns the backup codes.
Removing the prod console.warn in interceptOauthCallback that
logged when extractSessionToken returned null. Without a repro it
was noise; if the OAuth-no-session reports resurface we can wire
proper telemetry instead of an inline log.

Also dropping the StepIndicator JSDoc block in totp-setup-dialog;
the function name and STEP_DEFS are self-evident.
@joelorzet joelorzet requested review from a team, OleksandrUA, eskp and suisuss and removed request for a team May 26, 2026 21:34
@joelorzet joelorzet merged commit fa43d85 into prod May 26, 2026
23 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.

1 participant