Skip to content

Fix recovery passkey creation with locale-independent naming - #2480

Merged
MusabShakeel576 merged 1 commit into
masterfrom
claude/passkey-creation-recovery-jh2omx
Sep 7, 2026
Merged

Fix recovery passkey creation with locale-independent naming#2480
MusabShakeel576 merged 1 commit into
masterfrom
claude/passkey-creation-recovery-jh2omx

Conversation

@MusabShakeel576

Copy link
Copy Markdown
Contributor

Summary

Fixes the recovery passkey flow to use locale-independent naming and properly handle session expiration. The previous implementation used toLocaleDateString() which failed on non-Latin locales (Arabic, Persian, Bengali) and had day-level granularity causing duplicate name errors on retry attempts.

Key Changes

  • Locale-independent passkey naming: Replaced toLocaleDateString() with ISO timestamp format (Recovery Passkey - 2026-09-07T09:51:00.000Z) that is ASCII-safe and unique per millisecond

    • Added buildRecoveryPasskeyName() utility to generate compliant names
    • Added isValidTurnkeyPasskeyName() to validate names against Turnkey's pattern before submission
  • Session expiration handling: Added pre-prompt session validation to prevent orphaned passpkeys

    • Backend now returns expiresAt timestamp from verifyRecoveryOtp()
    • Added hasUsableSession() check with 60-second margin before attempting passkey creation
    • Users are sent back to OTP step if session expires, where "Resend code" issues a fresh one
  • Session error detection: Added isTurnkeySessionError() utility to distinguish session failures from retryable errors

    • Recognizes Turnkey error codes (SESSION_EXPIRED, NO_SESSION_FOUND)
    • Matches error message patterns for wrapped/indirect session failures
    • Recursively inspects error cause chain (SDK wraps errors up to 4 levels deep)
  • Error instrumentation: Added Sentry reporting for passkey creation failures with context (user ID, organization, session expiry, underlying cause)

Implementation Details

  • The recovery session is minted from a single-use code and cannot be renewed from the add-passkey screen, so validation happens before the prompt rather than after
  • The 60-second margin accounts for the passkey creation flow (platform sheet, biometrics, password manager) where a lapsed session leaves an orphaned passkey on the device that Turnkey never registers
  • Tests verify the new naming scheme works across locales and handles edge cases (cyclic error causes, missing errors)

https://claude.ai/code/session_01Us4dxLHKKdmSKQQ643mmaP

Three things kept the add-passkey step after a recovery from working,
and a fourth kept anyone from seeing it.

The name was built from `toLocaleDateString()`. Turnkey validates an
authenticator name against /^[a-zA-Z0-9 _\-:\/\.]{1,64}$/, and on an
Arabic, Persian or Bengali device that call returns non-Latin digits and
embedded RTL marks (ar-EG gives `٧‏/٩‏/٢٠٢٦`). The SDK rejected the name
before it ever showed the passkey prompt, so the step could not work at
all on those devices. Its granularity was also one day, so a second
attempt on the same date re-sent a name the account already held, which
Turnkey refuses - one failed attempt poisoned the rest of the day. An
ISO timestamp is ASCII by construction and unique per millisecond, which
is what the SDK itself does when no name is given.

The screen also offered a retry that could not succeed. The session is
minted from a single-use code and cannot be renewed here, so once it
lapses every tap fails the same way. It is now checked before the prompt
rather than after - a passkey created against a dead session is one
Turnkey never registers, and it stays on the device - and a session
failure sends the user back to the OTP step, where "Resend code" issues
a fresh one.

Finally, none of this was reportable. `addPasskey` collapses every
failure downstream of the prompt into a bare "Failed to add passkey" and
leaves the real Turnkey error on `cause`, and this flow only logged to
the console while the signup passkey flow captured to Sentry. That is
why the screenshot's error appears nowhere in Sentry. Capture it with
the Turnkey code and cause attached.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Us4dxLHKKdmSKQQ643mmaP
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
solid-app Ignored Ignored Sep 7, 2026 11:41am UTC
solid-app-staging Ignored Ignored Sep 7, 2026 11:41am UTC

Request Review

@MusabShakeel576
MusabShakeel576 merged commit 70bb0f5 into master Sep 7, 2026
5 checks passed
@claude

claude Bot commented Sep 7, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

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.

2 participants