Skip to content

feat(ai-byok): add @tanstack/ai-byok toolkit - #1144

Draft
tombeckenham wants to merge 20 commits into
byok-packagefrom
byok-package-standalone
Draft

feat(ai-byok): add @tanstack/ai-byok toolkit#1144
tombeckenham wants to merge 20 commits into
byok-packagefrom
byok-package-standalone

Conversation

@tombeckenham

Copy link
Copy Markdown
Contributor

🎯 Changes

Stacked on #906 (headless defineByok). This PR adds the optional @tanstack/ai-byok package — a client keyring, React UI, and stateless relay helpers. Keys stay in the browser and travel in x-byok-<provider> headers only.

Depends on #906. Merge that first, or retarget this to main after it lands.

  • @tanstack/ai-byokbyokHeaders, withByok / byokFetch (connection transport + byokMissing 401), byokFetcher (fetcher transport for useChat / useGeneration)
  • StoragememoryStorage() by default. defaultByokStorage() / passkeyStorage() encrypt the keyring with WebAuthn PRF → HKDF → AES-256-GCM in IndexedDB. After refresh, saved keys surface as locked until unlock(). No plaintext localStorage.
  • React (@tanstack/ai-byok/react) — <ByokProvider>, useByok(), drop-in <ByokKeyManager> (last four characters only)
  • OpenRouter PKCE (@tanstack/ai-byok/openrouter) — one-click OAuth; the returned key is stored like any other BYOK key
  • Server (@tanstack/ai-byok/server) — getByokKey, byokMissing, scrubSecrets / maskKey. Stateless pass-through; no persistence, no central endpoint
  • Examplets-react-chat key icon + env-key alerts; passkey storage when the browser supports it
  • E2Etesting/e2e/tests/byok.spec.ts covers header transport and the missing-key 401

Usage

import { ByokProvider, ByokKeyManager, useByok } from '@tanstack/ai-byok/react'
import { withByok, defaultByokStorage } from '@tanstack/ai-byok/react'
import { fetchServerSentEvents } from '@tanstack/ai-client'
import { useChat } from '@tanstack/ai-react'

<ByokProvider storage={defaultByokStorage()}>
  <ByokKeyManager />
</ByokProvider>
useChat({
  connection: fetchServerSentEvents(
    '/api/chat',
    withByok(() => keys, {
      onMissingKey: (provider) => openKeyDialog(provider),
    }),
  ),
})
import { getByokKey, byokMissing } from '@tanstack/ai-byok/server'

const apiKey = getByokKey(request, 'openai')
if (!apiKey) return byokMissing('openai')

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

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

tombeckenham and others added 20 commits August 19, 2026 06:38
Client keyring, per-provider request headers, and stateless server
helpers that never persist or log provider keys. Keys live client-side
and travel in an x-tanstack-byok-<provider> header, never the request
body or message history.

- @tanstack/ai-byok: provider registry, byokHeaders, pluggable storage
  (memory default, opt-in plaintext localStorage), validateKey
- /react: <ByokProvider storage>, useByok, drop-in <ByokKeyManager>
  (last-4 display only)
- /server: getByokKey (header-only, never logged), byokMissing (typed
  error), scrubSecrets/maskKey

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the plaintext localStorage tier with passkey-encrypted
persistence (WebAuthn PRF -> HKDF -> AES-256-GCM ciphertext in
IndexedDB), unwrapped on demand with a biometric/PIN tap. Fully
client-side; protects at-rest, not live in-page XSS (documented).

- passkeyStorage() + isPasskeyStorageSupported() feature detection
- KeyringStorage gains optional `unlockable` + `warning`
- ByokProvider: `locked`/`unlock` so unlockable storage never prompts
  on mount; hydrates on explicit unlock or first save
- ByokKeyManager: unlock banner + storage-specific warning
- memoryStorage() remains the default; no plaintext persistence exists

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

- New /byok route: ByokProvider (passkey storage where supported, else
  memory) + ByokKeyManager + a minimal chat that attaches byokHeaders(keys)
- New /api/byok-chat relay: reads the key via getByokKey(request, provider),
  builds the adapter with create{Openai,Anthropic,Gemini}Chat(model, apiKey),
  returns byokMissing() when absent — stateless, no persist/log
- passkeyStorage gains an rpId option; by default the passkey binds to the
  current origin (no hardcoded/central domain)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New testing/e2e /byok route + /api/byok-chat relay + byok.spec.ts:
- key rides in the x-tanstack-byok-openai header, is absent from the
  request body, streams the aimock response, and the manager shows only
  the last-4
- missing key → byokMissing 401 surfaced as an error, no answer produced

Keyring is hydrated via a preloaded storage (the same load() path passkey
storage uses), so the flow is deterministic without a live WebAuthn
ceremony (passkey crypto + locked/unlock are covered by package unit
tests). Adds an apiKeyOverride to the e2e createTextAdapter and a
byok-masked test hook to ByokKeyManager.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…v-key alerts

Replaces the standalone /byok demo page with front-page integration:
- key icon in the model bar opens a dark-themed dialog for per-provider
  keys (last-4 only), matching the app theme
- getEnvKeyStatus server fn reports which providers have a server env key
  (booleans only, never the value); the key icon shows an amber dot and a
  banner warns when the selected model's provider has no key
- api/tanchat prefers a per-request BYOK header key over env per provider
  (withByok helper); connection attaches byokHeaders(keys)

Removes the old /byok route + api.byok-chat.ts + nav link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ple uses passkey

Persistent storage can now report presence without decrypting, so the UI
knows keys exist after a refresh:
- KeyringStorage gains optional peek() → { provider: last-4 }
- passkeyStorage stores an unencrypted provider→last-4 sidecar next to the
  ciphertext and reads it via peek() with no unlock ceremony
- new KeyStatus 'locked'; ByokProvider peeks on mount to mark saved keys
  locked (with last-4); unlock() promotes them to 'set' on decrypt

Example front page now uses passkey-encrypted storage when a platform
authenticator is available (else memory). The key dialog shows locked keys
with a lock + last-4 and an "Unlock saved keys" action; the model-bar
warning distinguishes "no key" from "saved but locked → Unlock".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… on byokMissing

The SSE adapter throws a generic HTTP error on non-2xx without the body, so
useChat's error can't identify the missing provider. withByok wires the
provider's fetchClient option to peek at the relay's byokMissing 401 body:

- withByok(getKeys, { onMissingKey }) attaches byokHeaders per request and
  invokes onMissingKey(provider) when the relay returns byokMissing
- byokFetch is the lower-level fetch wrapper; both exported from root + /react
- root now re-exports isByokMissingBody / ByokMissingBody

Example: /api/tanchat returns byokMissing(provider) when it has no server env
key and no BYOK header (instead of a generic 500); the front page uses
withByok, and onMissingKey opens the (now controllable) key dialog focused on
that provider — or, if the key is saved-but-locked, calls unlock() instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BYOK header prefix, IndexedDB name, passkey relying-party name, and
HKDF label all hardcoded "tanstack". None of these need the vendor name:
the header is a private protocol between this package's own client and
server (both resolve it via byokHeaderName), and the storage identifiers
are already overridable per instance. Neutral defaults let the toolkit
read as reusable rather than TanStack-specific.

- header prefix:      x-tanstack-byok-        -> x-byok-
- IndexedDB default:  tanstack-byok           -> byok
- passkey rpName:     "TanStack AI BYOK"      -> "BYOK"
- HKDF info label:    tanstack-byok:keyring:v1 -> byok:keyring:v1

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors withByok (which targets the connection transport) for the fetcher
transport used by useChat/useGeneration. Hands the fetcher body fresh BYOK
headers + a missing-key-aware fetch, covering both a plain fetch call and a
TanStack Start server function (via call-site headers). Keys still travel in
the x-byok-<provider> header, never the body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add OpenRouter OAuth PKCE sign-in with React hooks and client helpers.
Document BYOK in the advanced guide and API reference, with nav cross-links.

Use defaultByokStorage() for passkey-encrypted persistence of all keys
(pasted and OpenRouter PKCE), falling back to session memory when passkeys
are unavailable. Remove the composite base/oauth storage split.

Wire the ts-react-chat example to defaultByokStorage and extend ByokKeyManager
with OpenRouter sign-in support.
…UI dedup

- Add preferByokAdapter/requireByokOrEnv server exports and use them in the example
- Extract buildByokRequestContext to dedupe withByok and byokFetcher
- Move isByokMissingBody to shared/, tighten provider guard, sanitize decrypted keyrings
- Split OpenRouter PKCE into @tanstack/ai-byok/openrouter subpath exports
- Extract ByokProviderRow, add ByokKeyDialog to package, remove duplicate example UI
Wrap doc snippets in complete handlers/components, use relative imports
for app-specific helpers, and remove orphan return statements so all 20
BYOK code fences pass kiira.
…th Zod

Drop preferByokAdapter/requireByokOrEnv in favor of getByokOrEnvKey so
relays pass a real apiKey into createAnthropicChat(model, apiKey). The
chat example parses forwarded provider/model against MODEL_OPTIONS and
returns 400 on unknown pairs instead of falling back to a default.
… keys

setKey/clearKey load and merge the stored ring while locked, so an
OpenRouter PKCE return cannot replace the ciphertext with a single key.
Persist failures roll back optimistic state; Validate/Clear/Save stay
disabled until unlock. Scrub the example relay catch and document PKCE
throws plus lastFour display-safety.
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 217af093-bbf7-44eb-96eb-9ad7414ebbe3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​vitest/​coverage-v8@​4.0.14991006998100

View full report

@nx-cloud

nx-cloud Bot commented Aug 19, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit ab56c9f

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

☁️ Nx Cloud last updated this comment at 2026-08-19 00:08:38 UTC

@pkg-pr-new

pkg-pr-new Bot commented Aug 19, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-byok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byok@1144

@tanstack/ai-byteplus

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-snippets

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

@tanstack/ai-codex

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

@tanstack/ai-cohere

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-daytona

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-isolate-quickjs-bun

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

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-perplexity

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vercel-gateway

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: ab56c9f

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