@tanstack/ai-client 0.25.2 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +2 -1
  2. package/dist/esm/byok/client.d.ts +52 -0
  3. package/dist/esm/byok/client.js +300 -0
  4. package/dist/esm/byok/client.js.map +1 -0
  5. package/dist/esm/byok/passkey.d.ts +43 -0
  6. package/dist/esm/byok/passkey.js +251 -0
  7. package/dist/esm/byok/passkey.js.map +1 -0
  8. package/dist/esm/byok/resolve.d.ts +8 -0
  9. package/dist/esm/byok/resolve.js +20 -0
  10. package/dist/esm/byok/resolve.js.map +1 -0
  11. package/dist/esm/byok/storage.d.ts +15 -0
  12. package/dist/esm/byok/storage.js +16 -0
  13. package/dist/esm/byok/storage.js.map +1 -0
  14. package/dist/esm/byok.d.ts +8 -0
  15. package/dist/esm/byok.js +5 -0
  16. package/dist/esm/chat-client.d.ts +18 -5
  17. package/dist/esm/chat-client.js +96 -59
  18. package/dist/esm/chat-client.js.map +1 -1
  19. package/dist/esm/connection-adapters.d.ts +2 -0
  20. package/dist/esm/connection-adapters.js +70 -30
  21. package/dist/esm/connection-adapters.js.map +1 -1
  22. package/dist/esm/generation-client.d.ts +3 -1
  23. package/dist/esm/generation-client.js +27 -6
  24. package/dist/esm/generation-client.js.map +1 -1
  25. package/dist/esm/generation-types.d.ts +14 -0
  26. package/dist/esm/generation-types.js +4 -2
  27. package/dist/esm/generation-types.js.map +1 -1
  28. package/dist/esm/interrupt-manager.d.ts +1 -0
  29. package/dist/esm/interrupt-manager.js +15 -6
  30. package/dist/esm/interrupt-manager.js.map +1 -1
  31. package/dist/esm/sse-parser.js +8 -1
  32. package/dist/esm/sse-parser.js.map +1 -1
  33. package/dist/esm/types.d.ts +46 -3
  34. package/dist/esm/types.js.map +1 -1
  35. package/dist/esm/video-generation-client.d.ts +3 -1
  36. package/dist/esm/video-generation-client.js +29 -8
  37. package/dist/esm/video-generation-client.js.map +1 -1
  38. package/package.json +7 -3
  39. package/src/byok/client.ts +360 -0
  40. package/src/byok/passkey.ts +399 -0
  41. package/src/byok/resolve.ts +30 -0
  42. package/src/byok/storage.ts +29 -0
  43. package/src/byok.ts +20 -0
  44. package/src/chat-client.ts +138 -77
  45. package/src/connection-adapters.ts +108 -53
  46. package/src/generation-client.ts +53 -9
  47. package/src/generation-types.ts +24 -2
  48. package/src/interrupt-manager.ts +20 -6
  49. package/src/sse-parser.ts +10 -0
  50. package/src/types.ts +48 -3
  51. package/src/video-generation-client.ts +50 -9
@@ -0,0 +1,251 @@
1
+ import { memoryStorage } from "./storage.js";
2
+ import { isProviderId } from "@tanstack/ai/byok";
3
+ //#region src/byok/passkey.ts
4
+ /**
5
+ * Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).
6
+ *
7
+ * The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived
8
+ * from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.
9
+ * Decryption happens entirely client-side with the user present — no server,
10
+ * no custodian.
11
+ *
12
+ * Honest scope: this protects against at-rest theft (stolen device,
13
+ * storage-dumping extension, backups). It does NOT defeat live in-page XSS —
14
+ * an attacker running JS in the origin after the user unlocks can read the
15
+ * decrypted keys from memory.
16
+ */
17
+ var STORE_NAME = "keyring";
18
+ var RECORD_ID = "default";
19
+ var HKDF_INFO = "byok:keyring:v1";
20
+ var DEFAULT_DB = "byok";
21
+ function sanitizeKeyring(value) {
22
+ if (typeof value !== "object" || value === null) return {};
23
+ const keys = {};
24
+ for (const [provider, key] of Object.entries(value)) if (isProviderId(provider) && typeof key === "string" && key.length > 0) keys[provider] = key;
25
+ return keys;
26
+ }
27
+ /** Build the non-sensitive `provider → last 4` preview from a keyring. */
28
+ function previewOf(keys) {
29
+ const preview = {};
30
+ for (const [provider, key] of Object.entries(keys)) {
31
+ if (!key || !isProviderId(provider)) continue;
32
+ preview[provider] = key.length > 4 ? key.slice(-4) : "";
33
+ }
34
+ return preview;
35
+ }
36
+ /**
37
+ * Whether the current environment exposes WebAuthn. Actual PRF support can
38
+ * only be confirmed during registration; `passkeyStorage` throws if the
39
+ * chosen authenticator does not support PRF.
40
+ */
41
+ function isPasskeyStorageSupported() {
42
+ return typeof globalThis !== "undefined" && typeof globalThis.PublicKeyCredential !== "undefined" && typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials.create === "function";
43
+ }
44
+ /** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */
45
+ async function deriveAesKey(prfOutput) {
46
+ const base = await crypto.subtle.importKey("raw", prfOutput, "HKDF", false, ["deriveKey"]);
47
+ return crypto.subtle.deriveKey({
48
+ name: "HKDF",
49
+ hash: "SHA-256",
50
+ salt: /* @__PURE__ */ new Uint8Array(0),
51
+ info: new TextEncoder().encode(HKDF_INFO)
52
+ }, base, {
53
+ name: "AES-GCM",
54
+ length: 256
55
+ }, false, ["encrypt", "decrypt"]);
56
+ }
57
+ async function encryptKeyring(key, keys) {
58
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
59
+ const plaintext = new TextEncoder().encode(JSON.stringify(keys));
60
+ const ciphertext = await crypto.subtle.encrypt({
61
+ name: "AES-GCM",
62
+ iv
63
+ }, key, plaintext);
64
+ return {
65
+ iv: iv.buffer,
66
+ ciphertext
67
+ };
68
+ }
69
+ async function decryptKeyring(key, iv, ciphertext) {
70
+ const plaintext = await crypto.subtle.decrypt({
71
+ name: "AES-GCM",
72
+ iv
73
+ }, key, ciphertext);
74
+ return sanitizeKeyring(JSON.parse(new TextDecoder().decode(plaintext)));
75
+ }
76
+ function openDb(dbName) {
77
+ return new Promise((resolve, reject) => {
78
+ const request = indexedDB.open(dbName, 1);
79
+ request.onupgradeneeded = () => {
80
+ request.result.createObjectStore(STORE_NAME, { keyPath: "id" });
81
+ };
82
+ request.onsuccess = () => resolve(request.result);
83
+ request.onerror = () => reject(request.error);
84
+ });
85
+ }
86
+ function idbGet(dbName) {
87
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
88
+ const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(RECORD_ID);
89
+ request.onsuccess = () => resolve(request.result ?? null);
90
+ request.onerror = () => reject(request.error);
91
+ }));
92
+ }
93
+ function idbPut(dbName, record) {
94
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
95
+ const tx = db.transaction(STORE_NAME, "readwrite");
96
+ tx.objectStore(STORE_NAME).put(record);
97
+ tx.oncomplete = () => resolve();
98
+ tx.onerror = () => reject(tx.error);
99
+ }));
100
+ }
101
+ function idbClear(dbName) {
102
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
103
+ const tx = db.transaction(STORE_NAME, "readwrite");
104
+ tx.objectStore(STORE_NAME).delete(RECORD_ID);
105
+ tx.oncomplete = () => resolve();
106
+ tx.onerror = () => reject(tx.error);
107
+ }));
108
+ }
109
+ function requirePublicKeyCredential(credential, action) {
110
+ if (!credential) throw new Error(`Passkey ${action} was cancelled`);
111
+ if (!(credential instanceof PublicKeyCredential)) throw new Error(`Unexpected credential type during ${action}`);
112
+ return credential;
113
+ }
114
+ async function registerPasskey(rpName, userName, rpId) {
115
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
116
+ const credential = requirePublicKeyCredential(await navigator.credentials.create({ publicKey: {
117
+ challenge: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)),
118
+ rp: rpId ? {
119
+ name: rpName,
120
+ id: rpId
121
+ } : { name: rpName },
122
+ user: {
123
+ id: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)),
124
+ name: userName,
125
+ displayName: userName
126
+ },
127
+ pubKeyCredParams: [{
128
+ type: "public-key",
129
+ alg: -7
130
+ }, {
131
+ type: "public-key",
132
+ alg: -257
133
+ }],
134
+ authenticatorSelection: {
135
+ residentKey: "required",
136
+ userVerification: "required"
137
+ },
138
+ extensions: { prf: { eval: { first: salt } } }
139
+ } }), "registration");
140
+ const prf = credential.getClientExtensionResults().prf;
141
+ if (!prf?.enabled) throw new Error("This authenticator does not support the WebAuthn PRF extension");
142
+ return {
143
+ credentialId: credential.rawId,
144
+ salt,
145
+ prf: prf.results?.first
146
+ };
147
+ }
148
+ async function evaluatePrf(credentialId, salt) {
149
+ const result = requirePublicKeyCredential(await navigator.credentials.get({ publicKey: {
150
+ challenge: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)),
151
+ allowCredentials: [{
152
+ type: "public-key",
153
+ id: credentialId
154
+ }],
155
+ userVerification: "required",
156
+ extensions: { prf: { eval: { first: salt } } }
157
+ } }), "unlock").getClientExtensionResults().prf?.results?.first;
158
+ if (!result) throw new Error("Authenticator did not return a PRF result");
159
+ return result;
160
+ }
161
+ /**
162
+ * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so
163
+ * nothing is decrypted until the user calls `unlock()` (or saves a key, which
164
+ * registers a passkey on first use). The derived key is cached in memory for
165
+ * the session so repeated saves don't re-prompt.
166
+ */
167
+ function passkeyStorage(options = {}) {
168
+ const rpName = options.rpName ?? "BYOK";
169
+ const userName = options.userName ?? "byok-keyring";
170
+ const { rpId } = options;
171
+ const dbName = options.dbName ?? DEFAULT_DB;
172
+ let cachedKey = null;
173
+ let cachedMeta = null;
174
+ async function ensureKey() {
175
+ if (cachedKey && cachedMeta) return {
176
+ key: cachedKey,
177
+ ...cachedMeta
178
+ };
179
+ const existing = await idbGet(dbName);
180
+ if (existing) {
181
+ cachedKey = await deriveAesKey(await evaluatePrf(existing.credentialId, existing.salt));
182
+ cachedMeta = {
183
+ credentialId: existing.credentialId,
184
+ salt: new Uint8Array(existing.salt)
185
+ };
186
+ } else {
187
+ const reg = await registerPasskey(rpName, userName, rpId);
188
+ cachedKey = await deriveAesKey(reg.prf ?? await evaluatePrf(reg.credentialId, reg.salt));
189
+ cachedMeta = {
190
+ credentialId: reg.credentialId,
191
+ salt: reg.salt
192
+ };
193
+ }
194
+ return {
195
+ key: cachedKey,
196
+ ...cachedMeta
197
+ };
198
+ }
199
+ return {
200
+ id: "passkey",
201
+ label: "Passkey-encrypted (this device)",
202
+ persistent: true,
203
+ unlockable: true,
204
+ warning: "Keys are encrypted with your passkey and unlocked with biometrics. This protects saved keys if your device is stolen, but not against code running on this page after you unlock.",
205
+ peek: async () => {
206
+ return (await idbGet(dbName))?.preview ?? {};
207
+ },
208
+ load: async () => {
209
+ const existing = await idbGet(dbName);
210
+ if (!existing) return {};
211
+ const { key } = await ensureKey();
212
+ return decryptKeyring(key, existing.iv, existing.ciphertext);
213
+ },
214
+ save: async (keys) => {
215
+ const existing = await idbGet(dbName);
216
+ if (!Object.values(keys).some(Boolean) && !existing) return;
217
+ const { key, credentialId, salt } = await ensureKey();
218
+ const { iv, ciphertext } = await encryptKeyring(key, keys);
219
+ await idbPut(dbName, {
220
+ id: RECORD_ID,
221
+ credentialId,
222
+ salt: salt.buffer,
223
+ iv,
224
+ ciphertext,
225
+ preview: previewOf(keys)
226
+ });
227
+ },
228
+ clear: async () => {
229
+ cachedKey = null;
230
+ cachedMeta = null;
231
+ await idbClear(dbName);
232
+ }
233
+ };
234
+ }
235
+ /**
236
+ * Passkey-encrypted storage when WebAuthn is available in a secure context.
237
+ * Otherwise session memory, with a warning — this is not an automatic PRF
238
+ * fallback. First save still throws if the authenticator lacks PRF.
239
+ */
240
+ function defaultByokStorage(options) {
241
+ const secure = typeof globalThis.isSecureContext !== "boolean" || globalThis.isSecureContext;
242
+ if (!isPasskeyStorageSupported() || !secure) return {
243
+ ...memoryStorage(),
244
+ warning: "Passkeys are unavailable in this context. Keys stay in memory for this tab only."
245
+ };
246
+ return passkeyStorage(options);
247
+ }
248
+ //#endregion
249
+ export { decryptKeyring, defaultByokStorage, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage };
250
+
251
+ //# sourceMappingURL=passkey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey.js","names":[],"sources":["../../../src/byok/passkey.ts"],"sourcesContent":["import { isProviderId } from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { KeyPreview, Keyring, KeyringStorage } from './storage'\n\n/**\n * Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).\n *\n * The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived\n * from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.\n * Decryption happens entirely client-side with the user present — no server,\n * no custodian.\n *\n * Honest scope: this protects against at-rest theft (stolen device,\n * storage-dumping extension, backups). It does NOT defeat live in-page XSS —\n * an attacker running JS in the origin after the user unlocks can read the\n * decrypted keys from memory.\n */\n\nconst STORE_NAME = 'keyring'\nconst RECORD_ID = 'default'\nconst HKDF_INFO = 'byok:keyring:v1'\nconst DEFAULT_DB = 'byok'\n\ninterface StoredRecord {\n id: string\n /** The passkey's raw credential id, replayed in the unlock ceremony. */\n credentialId: ArrayBuffer\n /** Fixed per-install PRF evaluation input (not secret). */\n salt: ArrayBuffer\n /** AES-GCM initialization vector for this ciphertext. */\n iv: ArrayBuffer\n /** Encrypted keyring JSON. */\n ciphertext: ArrayBuffer\n /**\n * Unencrypted presence metadata (`provider → last 4`). Non-sensitive, so it\n * can be read via {@link KeyringStorage.peek} without an unlock ceremony to\n * show saved keys as \"locked\" after a refresh.\n */\n preview: KeyPreview\n}\n\nfunction sanitizeKeyring(value: unknown): Keyring {\n if (typeof value !== 'object' || value === null) return {}\n const keys: Keyring = {}\n for (const [provider, key] of Object.entries(value)) {\n if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {\n keys[provider] = key\n }\n }\n return keys\n}\n\n/** Build the non-sensitive `provider → last 4` preview from a keyring. */\nfunction previewOf(keys: Keyring): KeyPreview {\n const preview: KeyPreview = {}\n for (const [provider, key] of Object.entries(keys)) {\n if (!key || !isProviderId(provider)) continue\n // Keys of length ≤ 4 would make last-4 the whole secret — store presence only.\n preview[provider] = key.length > 4 ? key.slice(-4) : ''\n }\n return preview\n}\n\n/**\n * Whether the current environment exposes WebAuthn. Actual PRF support can\n * only be confirmed during registration; `passkeyStorage` throws if the\n * chosen authenticator does not support PRF.\n */\nexport function isPasskeyStorageSupported(): boolean {\n return (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.PublicKeyCredential !== 'undefined' &&\n typeof globalThis.navigator !== 'undefined' &&\n typeof globalThis.navigator.credentials.create === 'function'\n )\n}\n\n// ---------------------------------------------------------------------------\n// Crypto (exported for testing; the WebAuthn ceremony below feeds `deriveAesKey`)\n// ---------------------------------------------------------------------------\n\n/** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */\nexport async function deriveAesKey(\n prfOutput: BufferSource,\n): Promise<CryptoKey> {\n const base = await crypto.subtle.importKey('raw', prfOutput, 'HKDF', false, [\n 'deriveKey',\n ])\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: new Uint8Array(0),\n info: new TextEncoder().encode(HKDF_INFO),\n },\n base,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport async function encryptKeyring(\n key: CryptoKey,\n keys: Keyring,\n): Promise<{ iv: ArrayBuffer; ciphertext: ArrayBuffer }> {\n const iv = crypto.getRandomValues(new Uint8Array(12))\n const plaintext = new TextEncoder().encode(JSON.stringify(keys))\n const ciphertext = await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv },\n key,\n plaintext,\n )\n return { iv: iv.buffer, ciphertext }\n}\n\nexport async function decryptKeyring(\n key: CryptoKey,\n iv: BufferSource,\n ciphertext: BufferSource,\n): Promise<Keyring> {\n const plaintext = await crypto.subtle.decrypt(\n { name: 'AES-GCM', iv },\n key,\n ciphertext,\n )\n const parsed: unknown = JSON.parse(new TextDecoder().decode(plaintext))\n return sanitizeKeyring(parsed)\n}\n\n// ---------------------------------------------------------------------------\n// IndexedDB\n// ---------------------------------------------------------------------------\n\nfunction openDb(dbName: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(dbName, 1)\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE_NAME, { keyPath: 'id' })\n }\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction idbGet(dbName: string): Promise<StoredRecord | null> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const request = db\n .transaction(STORE_NAME, 'readonly')\n .objectStore(STORE_NAME)\n .get(RECORD_ID)\n request.onsuccess = () => resolve(request.result ?? null)\n request.onerror = () => reject(request.error)\n }),\n )\n}\n\nfunction idbPut(dbName: string, record: StoredRecord): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).put(record)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\nfunction idbClear(dbName: string): Promise<void> {\n return openDb(dbName).then(\n (db) =>\n new Promise((resolve, reject) => {\n const tx = db.transaction(STORE_NAME, 'readwrite')\n tx.objectStore(STORE_NAME).delete(RECORD_ID)\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n }),\n )\n}\n\n// ---------------------------------------------------------------------------\n// WebAuthn ceremonies\n// ---------------------------------------------------------------------------\n\nfunction requirePublicKeyCredential(\n credential: Credential | null,\n action: string,\n): PublicKeyCredential {\n if (!credential) throw new Error(`Passkey ${action} was cancelled`)\n if (!(credential instanceof PublicKeyCredential)) {\n throw new Error(`Unexpected credential type during ${action}`)\n }\n return credential\n}\n\nasync function registerPasskey(\n rpName: string,\n userName: string,\n rpId?: string,\n): Promise<{\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n prf?: BufferSource\n}> {\n const salt = crypto.getRandomValues(new Uint8Array(32))\n const credential = requirePublicKeyCredential(\n await navigator.credentials.create({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n // Omit `id` to let the browser bind the passkey to the current origin's\n // effective domain; set it to scope across subdomains of a self-host.\n rp: rpId ? { name: rpName, id: rpId } : { name: rpName },\n user: {\n id: crypto.getRandomValues(new Uint8Array(16)),\n name: userName,\n displayName: userName,\n },\n pubKeyCredParams: [\n { type: 'public-key', alg: -7 },\n { type: 'public-key', alg: -257 },\n ],\n authenticatorSelection: {\n residentKey: 'required',\n userVerification: 'required',\n },\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'registration',\n )\n\n const prf = credential.getClientExtensionResults().prf\n if (!prf?.enabled) {\n throw new Error(\n 'This authenticator does not support the WebAuthn PRF extension',\n )\n }\n return { credentialId: credential.rawId, salt, prf: prf.results?.first }\n}\n\nasync function evaluatePrf(\n credentialId: BufferSource,\n salt: BufferSource,\n): Promise<BufferSource> {\n const credential = requirePublicKeyCredential(\n await navigator.credentials.get({\n publicKey: {\n challenge: crypto.getRandomValues(new Uint8Array(32)),\n allowCredentials: [{ type: 'public-key', id: credentialId }],\n userVerification: 'required',\n extensions: { prf: { eval: { first: salt } } },\n },\n }),\n 'unlock',\n )\n const result = credential.getClientExtensionResults().prf?.results?.first\n if (!result) {\n throw new Error('Authenticator did not return a PRF result')\n }\n return result\n}\n\n// ---------------------------------------------------------------------------\n// Storage strategy\n// ---------------------------------------------------------------------------\n\nexport interface PasskeyStorageOptions {\n /** Relying-party name shown in the passkey prompt. */\n rpName?: string\n /** Username label attached to the created passkey. */\n userName?: string\n /**\n * WebAuthn Relying Party ID. Omit to bind the passkey to the current origin's\n * effective domain (the default — no central/hardcoded domain). Set it to a\n * registrable parent domain to share the credential across subdomains of your\n * own deployment. The encrypted keyring is never portable across unrelated\n * domains.\n */\n rpId?: string\n /** IndexedDB database name. Defaults to `byok`. */\n dbName?: string\n}\n\n/**\n * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so\n * nothing is decrypted until the user calls `unlock()` (or saves a key, which\n * registers a passkey on first use). The derived key is cached in memory for\n * the session so repeated saves don't re-prompt.\n */\nexport function passkeyStorage(\n options: PasskeyStorageOptions = {},\n): KeyringStorage {\n const rpName = options.rpName ?? 'BYOK'\n const userName = options.userName ?? 'byok-keyring'\n const { rpId } = options\n const dbName = options.dbName ?? DEFAULT_DB\n\n let cachedKey: CryptoKey | null = null\n let cachedMeta: {\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n } | null = null\n\n // Obtain the AES key, running exactly one WebAuthn ceremony if it isn't\n // already cached for this session (unlock if a passkey exists, else register).\n async function ensureKey(): Promise<{\n key: CryptoKey\n credentialId: ArrayBuffer\n salt: Uint8Array<ArrayBuffer>\n }> {\n if (cachedKey && cachedMeta) {\n return { key: cachedKey, ...cachedMeta }\n }\n const existing = await idbGet(dbName)\n if (existing) {\n const prf = await evaluatePrf(existing.credentialId, existing.salt)\n cachedKey = await deriveAesKey(prf)\n cachedMeta = {\n credentialId: existing.credentialId,\n salt: new Uint8Array(existing.salt),\n }\n } else {\n const reg = await registerPasskey(rpName, userName, rpId)\n const prf = reg.prf ?? (await evaluatePrf(reg.credentialId, reg.salt))\n cachedKey = await deriveAesKey(prf)\n cachedMeta = { credentialId: reg.credentialId, salt: reg.salt }\n }\n return { key: cachedKey, ...cachedMeta }\n }\n\n return {\n id: 'passkey',\n label: 'Passkey-encrypted (this device)',\n persistent: true,\n unlockable: true,\n warning:\n 'Keys are encrypted with your passkey and unlocked with biometrics. ' +\n 'This protects saved keys if your device is stolen, but not against code ' +\n 'running on this page after you unlock.',\n peek: async () => {\n // Unencrypted last-4 sidecar — no key material, no unlock ceremony.\n const existing = await idbGet(dbName)\n return existing?.preview ?? {}\n },\n load: async () => {\n const existing = await idbGet(dbName)\n if (!existing) return {}\n const { key } = await ensureKey()\n return decryptKeyring(key, existing.iv, existing.ciphertext)\n },\n save: async (keys) => {\n const existing = await idbGet(dbName)\n const hasKeys = Object.values(keys).some(Boolean)\n // First save with an empty keyring is a no-op — avoids a passkey ceremony\n // when another storage tier writes an empty ring.\n if (!hasKeys && !existing) return\n\n const { key, credentialId, salt } = await ensureKey()\n const { iv, ciphertext } = await encryptKeyring(key, keys)\n await idbPut(dbName, {\n id: RECORD_ID,\n credentialId,\n salt: salt.buffer,\n iv,\n ciphertext,\n preview: previewOf(keys),\n })\n },\n clear: async () => {\n cachedKey = null\n cachedMeta = null\n await idbClear(dbName)\n },\n }\n}\n\n/**\n * Passkey-encrypted storage when WebAuthn is available in a secure context.\n * Otherwise session memory, with a warning — this is not an automatic PRF\n * fallback. First save still throws if the authenticator lacks PRF.\n */\nexport function defaultByokStorage(\n options?: PasskeyStorageOptions,\n): KeyringStorage {\n const secure =\n typeof globalThis.isSecureContext !== 'boolean' ||\n globalThis.isSecureContext\n if (!isPasskeyStorageSupported() || !secure) {\n return {\n ...memoryStorage(),\n warning:\n 'Passkeys are unavailable in this context. Keys stay in memory for this tab only.',\n }\n }\n return passkeyStorage(options)\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { ProviderId } from '@tanstack/ai/byok';
2
+ import { ByokClient } from './client.js';
3
+ export declare function resolveByokProviderId(byokProvider: (() => string | undefined) | undefined, ...candidates: Array<unknown>): ProviderId | undefined;
4
+ /**
5
+ * Prepare and stamp headers for one resolved slug. Throws instead of
6
+ * attaching every stored key when the slug is missing.
7
+ */
8
+ export declare function prepareResolvedByokHeaders(byok: ByokClient, provider: ProviderId | undefined): Promise<Record<string, string>>;
@@ -0,0 +1,20 @@
1
+ import { ByokUnresolvedProviderError, isProviderId } from "@tanstack/ai/byok";
2
+ //#region src/byok/resolve.ts
3
+ function resolveByokProviderId(byokProvider, ...candidates) {
4
+ const fromFn = byokProvider?.();
5
+ if (isProviderId(fromFn)) return fromFn;
6
+ for (const candidate of candidates) if (isProviderId(candidate)) return candidate;
7
+ }
8
+ /**
9
+ * Prepare and stamp headers for one resolved slug. Throws instead of
10
+ * attaching every stored key when the slug is missing.
11
+ */
12
+ async function prepareResolvedByokHeaders(byok, provider) {
13
+ if (!provider) throw new ByokUnresolvedProviderError();
14
+ await byok.prepare(provider);
15
+ return byok.headers(provider);
16
+ }
17
+ //#endregion
18
+ export { prepareResolvedByokHeaders, resolveByokProviderId };
19
+
20
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","names":[],"sources":["../../../src/byok/resolve.ts"],"sourcesContent":["import { ByokUnresolvedProviderError, isProviderId } from '@tanstack/ai/byok'\nimport type { ProviderId } from '@tanstack/ai/byok'\nimport type { ByokClient } from './client'\n\nexport function resolveByokProviderId(\n byokProvider: (() => string | undefined) | undefined,\n ...candidates: Array<unknown>\n): ProviderId | undefined {\n const fromFn = byokProvider?.()\n if (isProviderId(fromFn)) return fromFn\n for (const candidate of candidates) {\n if (isProviderId(candidate)) return candidate\n }\n return undefined\n}\n\n/**\n * Prepare and stamp headers for one resolved slug. Throws instead of\n * attaching every stored key when the slug is missing.\n */\nexport async function prepareResolvedByokHeaders(\n byok: ByokClient,\n provider: ProviderId | undefined,\n): Promise<Record<string, string>> {\n if (!provider) {\n throw new ByokUnresolvedProviderError()\n }\n await byok.prepare(provider)\n return byok.headers(provider)\n}\n"],"mappings":";;AAIA,SAAgB,sBACd,cACA,GAAG,YACqB;CACxB,MAAM,SAAS,eAAe;CAC9B,IAAI,aAAa,MAAM,GAAG,OAAO;CACjC,KAAK,MAAM,aAAa,YACtB,IAAI,aAAa,SAAS,GAAG,OAAO;AAGxC;;;;;AAMA,eAAsB,2BACpB,MACA,UACiC;CACjC,IAAI,CAAC,UACH,MAAM,IAAI,4BAA4B;CAExC,MAAM,KAAK,QAAQ,QAAQ;CAC3B,OAAO,KAAK,QAAQ,QAAQ;AAC9B"}
@@ -0,0 +1,15 @@
1
+ import { ProviderId } from '@tanstack/ai/byok';
2
+ export type Keyring = Partial<Record<ProviderId, string>>;
3
+ export type KeyPreview = Partial<Record<ProviderId, string>>;
4
+ export interface KeyringStorage {
5
+ readonly id: string;
6
+ readonly label: string;
7
+ readonly persistent: boolean;
8
+ readonly unlockable?: boolean;
9
+ readonly warning?: string;
10
+ peek?: () => KeyPreview | Promise<KeyPreview>;
11
+ load: () => Keyring | Promise<Keyring>;
12
+ save: (keys: Keyring) => void | Promise<void>;
13
+ clear: () => void | Promise<void>;
14
+ }
15
+ export declare function memoryStorage(): KeyringStorage;
@@ -0,0 +1,16 @@
1
+ //#region src/byok/storage.ts
2
+ function memoryStorage() {
3
+ return {
4
+ id: "memory",
5
+ label: "Session only (not saved)",
6
+ persistent: false,
7
+ warning: "Keys stay in memory for this tab only. They are not saved across reloads.",
8
+ load: () => ({}),
9
+ save: () => {},
10
+ clear: () => {}
11
+ };
12
+ }
13
+ //#endregion
14
+ export { memoryStorage };
15
+
16
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","names":[],"sources":["../../../src/byok/storage.ts"],"sourcesContent":["import type { ProviderId } from '@tanstack/ai/byok'\n\nexport type Keyring = Partial<Record<ProviderId, string>>\nexport type KeyPreview = Partial<Record<ProviderId, string>>\n\nexport interface KeyringStorage {\n readonly id: string\n readonly label: string\n readonly persistent: boolean\n readonly unlockable?: boolean\n readonly warning?: string\n peek?: () => KeyPreview | Promise<KeyPreview>\n load: () => Keyring | Promise<Keyring>\n save: (keys: Keyring) => void | Promise<void>\n clear: () => void | Promise<void>\n}\n\nexport function memoryStorage(): KeyringStorage {\n return {\n id: 'memory',\n label: 'Session only (not saved)',\n persistent: false,\n warning:\n 'Keys stay in memory for this tab only. They are not saved across reloads.',\n load: () => ({}),\n save: () => {},\n clear: () => {},\n }\n}\n"],"mappings":";AAiBA,SAAgB,gBAAgC;CAC9C,OAAO;EACL,IAAI;EACJ,OAAO;EACP,YAAY;EACZ,SACE;EACF,aAAa,CAAC;EACd,YAAY,CAAC;EACb,aAAa,CAAC;CAChB;AACF"}
@@ -0,0 +1,8 @@
1
+ export { defineByok, ByokClient, EMPTY_BYOK_SNAPSHOT } from './byok/client.js';
2
+ export type { ByokPrompt, ByokSnapshot, DefineByokOptions, KeyStatus, } from './byok/client.js';
3
+ export { defineByokProvider } from '@tanstack/ai/byok';
4
+ export type { ByokProvider, ByokProviderInit } from '@tanstack/ai/byok';
5
+ export { memoryStorage } from './byok/storage.js';
6
+ export type { KeyPreview, Keyring, KeyringStorage } from './byok/storage.js';
7
+ export { defaultByokStorage, decryptKeyring, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage, } from './byok/passkey.js';
8
+ export type { PasskeyStorageOptions } from './byok/passkey.js';
@@ -0,0 +1,5 @@
1
+ import { memoryStorage } from "./byok/storage.js";
2
+ import { ByokClient, EMPTY_BYOK_SNAPSHOT, defineByok } from "./byok/client.js";
3
+ import { decryptKeyring, defaultByokStorage, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage } from "./byok/passkey.js";
4
+ import { defineByokProvider } from "@tanstack/ai/byok";
5
+ export { ByokClient, EMPTY_BYOK_SNAPSHOT, decryptKeyring, defaultByokStorage, defineByok, defineByokProvider, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, memoryStorage, passkeyStorage };
@@ -1,4 +1,5 @@
1
1
  import { AnyClientTool, InterruptDefinition, ModelMessage, RunAgentResumeItem, StreamChunk } from '@tanstack/ai/client';
2
+ import { ByokClient } from './byok.js';
2
3
  import { ConnectionAdapter } from './connection-adapters.js';
3
4
  import { BoundInterrupts, ChatClientOptions, ChatClientState, ChatFetcher, ChatInterruptState, ResolvableChatInterrupt, ChatResumeState, ConnectionStatus, MultimodalContent, QueueOption, QueueStrategy, QueuedMessage, SendMessageOptions, UIMessage, WhenBusy } from './types.js';
4
5
  type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClientTool>, TInterrupts extends ReadonlyArray<InterruptDefinition<any, any, any, any>> = readonly []> = {
@@ -7,6 +8,8 @@ type ChatClientUpdateOptionsWithoutContext<TTools extends ReadonlyArray<AnyClien
7
8
  /** @deprecated Use `forwardedProps` instead. */
8
9
  body?: Record<string, any>;
9
10
  forwardedProps?: Record<string, any>;
11
+ byok?: ByokClient;
12
+ byokProvider?: () => string | undefined;
10
13
  tools?: TTools;
11
14
  interrupts?: TInterrupts;
12
15
  queue?: QueueOption;
@@ -67,6 +70,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
67
70
  private activeResumeRunId;
68
71
  private bodyOption;
69
72
  private forwardedPropsOption;
73
+ private byok;
74
+ private byokProvider;
70
75
  private context;
71
76
  private pendingMessageBody;
72
77
  private queueConfig;
@@ -320,8 +325,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
320
325
  * - A MultimodalContent object with content array and optional custom ID
321
326
  * @param body - Optional body parameters to merge with the client's base body for this request.
322
327
  * Uses shallow merge with per-message body taking priority.
323
- * @param sendOptions - Per-call overrides, e.g. `{ whenBusy: 'interrupt' }` to
324
- * override the configured queue policy for this one send.
328
+ * @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
329
+ * queue policy for this one send. `{ body }`
330
+ * shallow-merges with `body` and with the chat-level
331
+ * `body` / `forwardedProps`. `sendOptions.body` wins
332
+ * on key collisions. Framework hooks forward this
333
+ * object as their second argument.
325
334
  *
326
335
  * @example
327
336
  * ```ts
@@ -331,9 +340,13 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
331
340
  * // Text message with custom body params
332
341
  * await client.sendMessage('Hello!', { temperature: 0.7 })
333
342
  *
334
- * // Per-call whenBusy override (body must still be the 2nd arg on ChatClient)
343
+ * // Per-call whenBusy override
335
344
  * await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
336
345
  *
346
+ * // Per-call body via options. Same effect as the positional arg.
347
+ * // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
348
+ * await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
349
+ *
337
350
  * // Multimodal message with image
338
351
  * await client.sendMessage({
339
352
  * content: [
@@ -378,8 +391,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
378
391
  private decideWhenBusy;
379
392
  private enqueueMessage;
380
393
  /**
381
- * Normalize the message input to extract content and optional id.
382
- * Trims string content automatically.
394
+ * Normalize the message input to extract content, optional id, and
395
+ * optional metadata. String form has no metadata. Trims string content.
383
396
  */
384
397
  private normalizeMessageInput;
385
398
  /**