Skip to content

fix(ai-client): normalize passkey PRF byte arrays - #1351

Merged
tannerlinsley merged 2 commits into
mainfrom
taren/byok-prf-byte-arrays
Sep 9, 2026
Merged

fix(ai-client): normalize passkey PRF byte arrays#1351
tannerlinsley merged 2 commits into
mainfrom
taren/byok-prf-byte-arrays

Conversation

@tannerlinsley

@tannerlinsley tannerlinsley commented Sep 9, 2026

Copy link
Copy Markdown
Member

Saving an API key with a 1Password passkey can fail in Chrome because its PRF result is a plain array. Validate those bytes and convert them to Uint8Array before Web Crypto imports the key.

🎯 Changes

  • Normalize only plain PRF arrays at the key-derivation boundary.
  • Reject malformed arrays before any encrypted record is saved.
  • Cover registration, follow-up prompts, reload, unlock, and resave.
  • Preserve the existing public API, encryption bytes, and storage format.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested code changes locally with pnpm run test:pr.
  • I fully understand the code in this pull request, including any code generated with AI assistance.
  • Docs: I updated docs/advanced/byok.md.
  • Changeset: I added a patch changeset for @tanstack/ai-client.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Root cause

Issue. BYOK save fails with importKey rejecting its key data when the passkey provider returns a plain array.

Cause. deriveAesKey passed the provider result directly to Web Crypto, which requires binary data.

Fix. Require exactly 32 integer bytes in the range 0 through 255, then convert the array without changing its bytes. Native binary values retain their existing path.

Possible alternatives

  • An unchecked conversion is shorter, but silently coerces malformed bytes and sparse arrays.
  • An app-only conversion leaves other SDK consumers broken.

Testing

  • pnpm test:pr: passed, including a rerun after syncing main.
  • Both passkey unit files: 17 passed.
  • Full browser suite before the metadata-only main sync: 681 passed, 1 unrelated ArkType test passed on retry, 1 skipped.

Agent-written regression, identical command in isolated worktrees:

vitest run tests/byok-passkey-ceremony.test.ts

main 551b58c4:
Tests 11 failed | 2 passed
Array-result saves fail at SubtleCrypto.importKey.

fixed 5d4b7ce9:
Tests 13 passed
Array-result saves succeed; native and array results unlock the same record.
Malformed arrays fail before saving.

Manual test:

  1. On the old version, save a BYOK key in Chrome with a 1Password passkey that returns an array PRF result.
  2. Repeat with this fix and confirm that save succeeds.
  3. Reload, unlock the key, and save an updated key.

The storage regression and Chrome BYOK test cover these paths with mocked WebAuthn and actual encryption. The browser test uses actual IndexedDB. Real 1Password prompts are not automated.

Risk / rollback

Scoped to plain-array PRF results. Byte-preservation tests check compatibility with native results. Revert this PR to undo the change.

Security and scope checks passed: no new dependencies, network calls, lifecycle scripts, CI permissions, or unrelated refactors. CodeRabbit: no PR existed during the local review.

Summary by CodeRabbit

  • New Features

    • BYOK passkey storage now supports valid 32-byte PRF values provided as byte arrays, preserving encryption key data during save and unlock.
    • Users can update and persist BYOK encryption keys through subsequent passkey ceremonies.
  • Bug Fixes

    • Invalid PRF arrays—including incorrect lengths, non-integer values, and out-of-range bytes—are rejected before being saved.
  • Documentation

    • Added guidance for passkey providers and password managers that return PRF values as byte arrays.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ab3df535-fbc4-4ea4-97e7-4f9e50d59f92

📥 Commits

Reviewing files that changed from the base of the PR and between 551b58c and 5d4b7ce.

📒 Files selected for processing (5)
  • .changeset/passkey-prf-byte-arrays.md
  • docs/advanced/byok.md
  • packages/ai-client/src/byok/passkey.ts
  • packages/ai-client/tests/byok-passkey-ceremony.test.ts
  • testing/e2e/tests/byok.spec.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The BYOK passkey flow now validates 32-byte PRF arrays, converts valid arrays to Uint8Array, and derives the same encryption key. Unit, ceremony, end-to-end, documentation, and changeset updates cover the behavior.

Changes

Passkey PRF byte-array support

Layer / File(s) Summary
PRF array validation and normalization
packages/ai-client/src/byok/passkey.ts, packages/ai-client/tests/byok-passkey-ceremony.test.ts
deriveAesKey validates array length and byte values, then converts valid arrays before HKDF derivation. The browser mock supports configurable PRF outputs.
Ceremony coverage and invalid-input checks
packages/ai-client/tests/byok-passkey-ceremony.test.ts
Tests verify unlock with native bytes and arrays. Invalid arrays are rejected before persistence.
End-to-end integration and documentation
testing/e2e/tests/byok.spec.ts, docs/advanced/byok.md, .changeset/passkey-prf-byte-arrays.md
The end-to-end mock returns array PRF results and verifies updated key persistence. Documentation and release metadata describe array support.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 5d4b7

This change accepts valid 32-byte passkey PRF arrays while preserving existing encryption and storage behavior, and rejects malformed results before saving. Registration, unlock, reload, resave, and invalid-input behavior are covered, with no current merge-blocking risk identified.

Suggested reviewers: jherr, tombeckenham

Sequence Diagram(s)

sequenceDiagram
  participant PasskeyProvider
  participant deriveAesKey
  participant CryptoSubtle
  participant BYOKStorage
  PasskeyProvider->>deriveAesKey: return PRF byte array
  deriveAesKey->>deriveAesKey: validate and convert array
  deriveAesKey->>CryptoSubtle: import normalized bytes for HKDF
  CryptoSubtle-->>deriveAesKey: derived AES key
  deriveAesKey->>BYOKStorage: save or load encrypted BYOK data
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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 2 functions across 3 files. (2 skipped: 2… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: normalizing passkey PRF byte arrays in the AI client.
Description check ✅ Passed The description includes the required Changes, Checklist, Release Impact, testing, root cause, alternatives, and risk sections. It explains the fix, validation, compatibility, documentation, changeset…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

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 2 functions across 3 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/byok-prf-byte-arrays

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tannerlinsley
tannerlinsley enabled auto-merge (squash) September 9, 2026 05:20
@nx-cloud

nx-cloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 5d4b7ce

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 28s View ↗

☁️ Nx Cloud last updated this comment at 2026-09-09 05:21:57 UTC

@pkg-pr-new

pkg-pr-new Bot commented Sep 9, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@1351

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@1351

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@1351

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@1351

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@1351

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@1351

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@1351

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@1351

@tanstack/ai-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cloudflare@1351

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@1351

@tanstack/ai-code-mode-snippets

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-snippets@1351

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@1351

@tanstack/ai-cohere

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-cohere@1351

@tanstack/ai-compaction

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-compaction@1351

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@1351

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@1351

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@1351

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@1351

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@1351

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@1351

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@1351

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@1351

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@1351

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@1351

@tanstack/ai-isolate-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-daytona@1351

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@1351

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@1351

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@1351

@tanstack/ai-llmgateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-llmgateway@1351

@tanstack/ai-lovable

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-lovable@1351

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@1351

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@1351

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@1351

@tanstack/ai-octane

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-octane@1351

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@1351

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@1351

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@1351

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@1351

@tanstack/ai-perplexity

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-perplexity@1351

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@1351

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@1351

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@1351

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@1351

@tanstack/ai-reactor

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-reactor@1351

@tanstack/ai-remix

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-remix@1351

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@1351

@tanstack/ai-sandbox-blaxel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-blaxel@1351

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@1351

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@1351

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@1351

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@1351

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@1351

@tanstack/ai-sandbox-upstash-box

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-upstash-box@1351

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@1351

@tanstack/ai-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-skills@1351

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@1351

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@1351

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@1351

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@1351

@tanstack/ai-vercel-gateway

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vercel-gateway@1351

@tanstack/ai-vertex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vertex@1351

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@1351

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@1351

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@1351

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@1351

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@1351

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@1351

@tanstack/svelte-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/svelte-ai-devtools@1351

commit: 5d4b7ce

@tannerlinsley
tannerlinsley merged commit 8689cb5 into main Sep 9, 2026
9 of 10 checks passed
@tannerlinsley
tannerlinsley deleted the taren/byok-prf-byte-arrays branch September 9, 2026 05:37
@github-actions github-actions Bot mentioned this pull request Sep 9, 2026
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