fix(ai-client): fail fast when a passkey unlock lacks user activation - #1332
fix(ai-client): fail fast when a passkey unlock lacks user activation#1332jherr wants to merge 2 commits into
Conversation
Passkey-encrypted BYOK stores the keyring in IndexedDB and decrypts it with a WebAuthn get() ceremony. Browsers that gate WebAuthn on transient user activation (Safari, Dia) silently suppress that prompt when get() runs outside a user gesture: no UI, and the call never resolves. An unlock buried in an async send pipeline therefore hangs forever. Check navigator.userActivation before the get() and throw a clear, catchable error instead. Callers should trigger unlock (byok.prepare()/unlock()) from the click handler, before awaiting other work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe passkey BYOK unlock flow now checks transient user activation before starting WebAuthn, exports the check, tests its behavior, and documents the required click-handler usage. ChangesPasskey activation handling
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to Passkey BYOK unlocks now fail immediately with a catchable error when a fresh browser user action is required, avoiding suppressed WebAuthn prompts that can hang. The behavior, public API, tests, and documentation are aligned, with no current merge-blocking risk identified. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 3 files. (3 skipped: 3 unsupported.)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
View your CI Pipeline Execution ↗ for commit e56ac80
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-byteplus
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-cloudflare
@tanstack/ai-code-mode
@tanstack/ai-code-mode-snippets
@tanstack/ai-codex
@tanstack/ai-cohere
@tanstack/ai-compaction
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-daytona
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-isolate-quickjs-bun
@tanstack/ai-llmgateway
@tanstack/ai-lovable
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-octane
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-perplexity
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-remix
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-upstash-box
@tanstack/ai-sandbox-vercel
@tanstack/ai-skills
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vercel-gateway
@tanstack/ai-vertex
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
@tanstack/svelte-ai-devtools
commit: |
Passkey BYOK stored a key you could save but not use. Saving worked. Sending a message hung with no prompt and no error. This PR makes the failed unlock throw a clear error instead of hanging forever.
🎯 Changes
passkeyStoragedecrypts the keyring with a WebAuthnget()ceremony. Safari and Dia only show that prompt while transient user activation is fresh (right after a click). They suppress it silently otherwise, andget()never resolves. An unlock that runs deep in an async send pipeline is past the activation window, so it hangs.evaluatePrfnow checksnavigator.userActivationbefore theget(). If activation is gone, it throws a clear, catchable error. Callers must trigger unlock (byok.prepare()/byok.unlock()) from the click handler, before awaiting other work.The companion app fix is TanStack/tanstack.com#1220.
✅ Checklist
pnpm run test:pr. See Testing for what I ran instead.docs/advanced/byok.md.🚀 Release Impact
Root cause
Issue. On tanstack.com/builder, a saved passkey-encrypted BYOK key never unlocks in Safari and Dia. The send stalls. The user sees repeated key prompts and no way forward.
Cause.
evaluatePrfinpackages/ai-client/src/byok/passkey.tscallsnavigator.credentials.get(). Those browsers gate WebAuthn on transient user activation. The unlock runs after the click's activation expires (~5s, or dropped across async work), so the browser shows no prompt and the promise never settles.Fix. Check
navigator.userActivation.isActivebefore theget(). If it is false, throw a clear error. The call fails fast instead of hanging, and the app can re-run it from a fresh gesture.Possible alternatives
get()after N seconds. Rejected: it also aborts a real prompt that the user is still reading, and it does not tell the caller why.passkeyStoragecan hit the same silent hang, so the library must fail loudly too.Testing
Commands run:
pnpm test:libinpackages/ai-client: 766 passed (49 files), including 2 new tests.pnpm test:typesinpackages/ai-client: clean.oxlint --type-awareon changed files: clean.pnpm test:docs: no broken links.pnpm test:prsweep. Ran the affected package checks above instead.Manual test (repro):
How this PR makes testing easy.
tests/byok-passkey.test.tscovers the guard: it throws when activation is gone, and passes when activation is active or the API is absent.Public API change
@tanstack/ai-client/byokexports a new helper,requireUserActivation. The behavior change that matters: trigger unlock from the click.Before
After
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Documentation