Uh oh!
There was an error while loading. Please reload this page.
feat(ai-byok): add @tanstack/ai-byok toolkit - #1144
Conversation
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.
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
View your CI Pipeline Execution ↗ for commit ab56c9f
☁️ Nx Cloud last updated this comment at |
@tanstack/ai@tanstack/ai-acp@tanstack/ai-angular@tanstack/ai-anthropic@tanstack/ai-bedrock@tanstack/ai-byok@tanstack/ai-byteplus@tanstack/ai-claude-code@tanstack/ai-client@tanstack/ai-code-mode@tanstack/ai-code-mode-snippets@tanstack/ai-codex@tanstack/ai-cohere@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-mcp@tanstack/ai-memory@tanstack/ai-mistral@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-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-vercel@tanstack/ai-solid@tanstack/ai-solid-ui@tanstack/ai-svelte@tanstack/ai-utils@tanstack/ai-vercel-gateway@tanstack/ai-vue@tanstack/ai-vue-ui@tanstack/openai-base@tanstack/preact-ai-devtools@tanstack/react-ai-devtools@tanstack/solid-ai-devtoolscommit: |
🎯 Changes
Stacked on #906 (headless
defineByok). This PR adds the optional@tanstack/ai-byokpackage — a client keyring, React UI, and stateless relay helpers. Keys stay in the browser and travel inx-byok-<provider>headers only.Depends on #906. Merge that first, or retarget this to
mainafter it lands.@tanstack/ai-byok—byokHeaders,withByok/byokFetch(connection transport +byokMissing401),byokFetcher(fetcher transport foruseChat/useGeneration)memoryStorage()by default.defaultByokStorage()/passkeyStorage()encrypt the keyring with WebAuthn PRF → HKDF → AES-256-GCM in IndexedDB. After refresh, saved keys surface aslockeduntilunlock(). No plaintext localStorage.@tanstack/ai-byok/react) —<ByokProvider>,useByok(), drop-in<ByokKeyManager>(last four characters only)@tanstack/ai-byok/openrouter) — one-click OAuth; the returned key is stored like any other BYOK key@tanstack/ai-byok/server) —getByokKey,byokMissing,scrubSecrets/maskKey. Stateless pass-through; no persistence, no central endpointts-react-chatkey icon + env-key alerts; passkey storage when the browser supports ittesting/e2e/tests/byok.spec.tscovers header transport and the missing-key 401Usage
✅ Checklist
pnpm run test:pr.🚀 Release Impact