@tanstack/ai-client 0.26.0 → 0.29.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 (56) hide show
  1. package/dist/esm/byok/client.d.ts +52 -0
  2. package/dist/esm/byok/client.js +300 -0
  3. package/dist/esm/byok/client.js.map +1 -0
  4. package/dist/esm/byok/passkey.d.ts +43 -0
  5. package/dist/esm/byok/passkey.js +251 -0
  6. package/dist/esm/byok/passkey.js.map +1 -0
  7. package/dist/esm/byok/resolve.d.ts +8 -0
  8. package/dist/esm/byok/resolve.js +20 -0
  9. package/dist/esm/byok/resolve.js.map +1 -0
  10. package/dist/esm/byok/storage.d.ts +15 -0
  11. package/dist/esm/byok/storage.js +16 -0
  12. package/dist/esm/byok/storage.js.map +1 -0
  13. package/dist/esm/byok.d.ts +8 -0
  14. package/dist/esm/byok.js +5 -0
  15. package/dist/esm/chat-client.d.ts +19 -3
  16. package/dist/esm/chat-client.js +82 -42
  17. package/dist/esm/chat-client.js.map +1 -1
  18. package/dist/esm/connection-adapters.d.ts +2 -0
  19. package/dist/esm/connection-adapters.js +35 -10
  20. package/dist/esm/connection-adapters.js.map +1 -1
  21. package/dist/esm/generation-client.d.ts +3 -1
  22. package/dist/esm/generation-client.js +23 -4
  23. package/dist/esm/generation-client.js.map +1 -1
  24. package/dist/esm/generation-types.d.ts +14 -0
  25. package/dist/esm/generation-types.js.map +1 -1
  26. package/dist/esm/interrupt-manager.d.ts +1 -0
  27. package/dist/esm/interrupt-manager.js +15 -6
  28. package/dist/esm/interrupt-manager.js.map +1 -1
  29. package/dist/esm/message-date-normalizer.d.ts +3 -0
  30. package/dist/esm/message-date-normalizer.js +35 -0
  31. package/dist/esm/message-date-normalizer.js.map +1 -0
  32. package/dist/esm/sse-parser.js +8 -1
  33. package/dist/esm/sse-parser.js.map +1 -1
  34. package/dist/esm/storage-adapters.js +13 -1
  35. package/dist/esm/storage-adapters.js.map +1 -1
  36. package/dist/esm/types.d.ts +34 -3
  37. package/dist/esm/types.js.map +1 -1
  38. package/dist/esm/video-generation-client.d.ts +3 -1
  39. package/dist/esm/video-generation-client.js +25 -6
  40. package/dist/esm/video-generation-client.js.map +1 -1
  41. package/package.json +7 -3
  42. package/src/byok/client.ts +360 -0
  43. package/src/byok/passkey.ts +399 -0
  44. package/src/byok/resolve.ts +30 -0
  45. package/src/byok/storage.ts +29 -0
  46. package/src/byok.ts +20 -0
  47. package/src/chat-client.ts +165 -72
  48. package/src/connection-adapters.ts +46 -14
  49. package/src/generation-client.ts +48 -4
  50. package/src/generation-types.ts +16 -0
  51. package/src/interrupt-manager.ts +20 -6
  52. package/src/message-date-normalizer.ts +29 -0
  53. package/src/sse-parser.ts +10 -0
  54. package/src/storage-adapters.ts +13 -2
  55. package/src/types.ts +36 -3
  56. package/src/video-generation-client.ts +45 -4
@@ -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;
@@ -120,6 +125,8 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
120
125
  private processingResolve;
121
126
  private errorReportedGeneration;
122
127
  private streamGeneration;
128
+ private continuationGeneration;
129
+ private streamContinuationGeneration;
123
130
  private continuationSkipped;
124
131
  private draining;
125
132
  private sessionGenerating;
@@ -234,6 +241,7 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
234
241
  retryInterrupts(): void;
235
242
  /** Unsafe low-level resume escape hatch. Prefer bound interrupt methods. */
236
243
  resumeInterruptsUnsafe(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
244
+ private resumeInterruptsUnsafeForGeneration;
237
245
  /** @deprecated Use bound interrupt methods or resumeInterruptsUnsafe(). */
238
246
  resumeInterrupts(resume: Array<RunAgentResumeItem>, state?: ChatResumeState): Promise<boolean>;
239
247
  private submitInterruptBatch;
@@ -320,8 +328,12 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
320
328
  * - A MultimodalContent object with content array and optional custom ID
321
329
  * @param body - Optional body parameters to merge with the client's base body for this request.
322
330
  * 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.
331
+ * @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
332
+ * queue policy for this one send. `{ body }`
333
+ * shallow-merges with `body` and with the chat-level
334
+ * `body` / `forwardedProps`. `sendOptions.body` wins
335
+ * on key collisions. Framework hooks forward this
336
+ * object as their second argument.
325
337
  *
326
338
  * @example
327
339
  * ```ts
@@ -331,9 +343,13 @@ export declare class ChatClient<TTools extends ReadonlyArray<AnyClientTool> = an
331
343
  * // Text message with custom body params
332
344
  * await client.sendMessage('Hello!', { temperature: 0.7 })
333
345
  *
334
- * // Per-call whenBusy override (body must still be the 2nd arg on ChatClient)
346
+ * // Per-call whenBusy override
335
347
  * await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
336
348
  *
349
+ * // Per-call body via options. Same effect as the positional arg.
350
+ * // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
351
+ * await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
352
+ *
337
353
  * // Multimodal message with image
338
354
  * await client.sendMessage({
339
355
  * content: [
@@ -1,9 +1,12 @@
1
+ import { prepareResolvedByokHeaders, resolveByokProviderId } from "./byok/resolve.js";
1
2
  import { createNoOpChatDevtoolsBridge } from "./devtools-noop.js";
3
+ import { normalizeMessagesDates } from "./message-date-normalizer.js";
2
4
  import { fetcherToConnectionAdapter, getChunkRunId as getChunkRunId$1, normalizeConnectionAdapter } from "./connection-adapters.js";
3
5
  import { ChatPersistor } from "./client-persistor.js";
4
6
  import { ClearedStreamTracker } from "./cleared-stream-tracker.js";
5
7
  import { InterruptManager } from "./interrupt-manager.js";
6
8
  import { StreamProcessor, convertSchemaToJsonSchema, generateMessageId, isStandardSchema, mergeMetadata, normalizeToUIMessage, parseWithStandardSchema, restoreInboundChunk, tanstackMetadata } from "@tanstack/ai/client";
9
+ import { ByokBlockedError, ByokMissingError, ByokUnresolvedProviderError } from "@tanstack/ai/byok";
7
10
  //#region src/chat-client.ts
8
11
  function assertUniqueInterruptDefinitions(interrupts) {
9
12
  const ids = /* @__PURE__ */ new Set();
@@ -148,6 +151,8 @@ var ChatClient = class {
148
151
  activeResumeRunId = null;
149
152
  bodyOption = {};
150
153
  forwardedPropsOption = {};
154
+ byok;
155
+ byokProvider;
151
156
  context = void 0;
152
157
  pendingMessageBody = void 0;
153
158
  queueConfig;
@@ -201,6 +206,8 @@ var ChatClient = class {
201
206
  processingResolve = null;
202
207
  errorReportedGeneration = null;
203
208
  streamGeneration = 0;
209
+ continuationGeneration = 0;
210
+ streamContinuationGeneration = 0;
204
211
  continuationSkipped = false;
205
212
  draining = false;
206
213
  sessionGenerating = false;
@@ -228,6 +235,8 @@ var ChatClient = class {
228
235
  }
229
236
  this.bodyOption = options.body || {};
230
237
  this.forwardedPropsOption = options.forwardedProps || {};
238
+ this.byok = options.byok;
239
+ this.byokProvider = options.byokProvider;
231
240
  this.context = options.context;
232
241
  this.queueConfig = normalizeQueueOption(options.queue);
233
242
  this.connection = normalizeConnectionAdapter(resolveTransport(options));
@@ -327,6 +336,7 @@ var ChatClient = class {
327
336
  const clientTool = (this.activeClientTools ?? this.clientToolsRef.current).get(args.toolName);
328
337
  const executeFunc = clientTool?.execute;
329
338
  if (executeFunc) {
339
+ const continuationGeneration = this.continuationGeneration;
330
340
  const runEventContext = this.devtoolsBridge.getCurrentRunEventContext();
331
341
  const executionPromise = (async () => {
332
342
  try {
@@ -341,7 +351,7 @@ var ChatClient = class {
341
351
  tool: args.toolName,
342
352
  output,
343
353
  state: "output-available"
344
- }, clientTool, runEventContext);
354
+ }, clientTool, continuationGeneration, runEventContext);
345
355
  } catch (error) {
346
356
  await this.addToolResultForClientTool({
347
357
  toolCallId: args.toolCallId,
@@ -349,7 +359,7 @@ var ChatClient = class {
349
359
  output: null,
350
360
  state: "output-error",
351
361
  errorText: error.message
352
- }, clientTool, runEventContext);
362
+ }, clientTool, continuationGeneration, runEventContext);
353
363
  } finally {
354
364
  this.pendingToolExecutions.delete(args.toolCallId);
355
365
  }
@@ -485,7 +495,7 @@ var ChatClient = class {
485
495
  }
486
496
  if (this.disposed || !this.tailing) return;
487
497
  if (this.isLoading || this.abortController) return;
488
- if (result.messages.length > 0) this.processor.setMessages(result.messages);
498
+ if (result.messages.length > 0) this.processor.setMessages(normalizeMessagesDates(result.messages));
489
499
  if (result.interrupts && result.interrupts.pending.length > 0) this.applyResumeSnapshot({
490
500
  resumeState: {
491
501
  threadId: this.threadId,
@@ -643,10 +653,14 @@ var ChatClient = class {
643
653
  resumeInterruptsUnsafe(resume, state) {
644
654
  const target = state ?? this.lastResume;
645
655
  if (!target) return Promise.resolve(false);
656
+ return this.resumeInterruptsUnsafeForGeneration(resume, target, this.continuationGeneration);
657
+ }
658
+ resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration) {
659
+ if (continuationGeneration !== this.continuationGeneration) return Promise.resolve(false);
646
660
  if (this.isLoading) return new Promise((resolve, reject) => {
647
661
  this.queuePostStreamAction(async () => {
648
662
  try {
649
- resolve(await this.resumeInterruptsUnsafe(resume, target));
663
+ resolve(await this.resumeInterruptsUnsafeForGeneration(resume, target, continuationGeneration));
650
664
  } catch (error) {
651
665
  reject(error);
652
666
  }
@@ -662,6 +676,7 @@ var ChatClient = class {
662
676
  return this.resumeInterruptsUnsafe(resume, state);
663
677
  }
664
678
  async submitInterruptBatch(submission) {
679
+ const continuationGeneration = this.continuationGeneration;
665
680
  this.activeInterruptSubmission = submission;
666
681
  this.interruptSubmissionFailure = void 0;
667
682
  for (const resolution of submission.resolutions) {
@@ -670,12 +685,13 @@ var ChatClient = class {
670
685
  const approvalId = resolution.interruptId;
671
686
  this.processor.addToolApprovalResponse(approvalId, approved);
672
687
  }
673
- const resumed = await this.resumeInterruptsUnsafe([...submission.resolutions], {
688
+ const resumed = await this.resumeInterruptsUnsafeForGeneration([...submission.resolutions], {
674
689
  threadId: submission.threadId,
675
690
  runId: submission.interruptedRunId
676
- }).finally(() => {
677
- this.activeInterruptSubmission = void 0;
691
+ }, continuationGeneration).finally(() => {
692
+ if (this.activeInterruptSubmission === submission) this.activeInterruptSubmission = void 0;
678
693
  });
694
+ if (continuationGeneration !== this.continuationGeneration) return;
679
695
  const failure = this.takeInterruptSubmissionFailure();
680
696
  if (failure !== void 0) throw { errors: failure.errors };
681
697
  if (!resumed) throw new Error("Interrupt continuation could not be started.");
@@ -900,6 +916,7 @@ var ChatClient = class {
900
916
  threadId: this.threadId,
901
917
  runId
902
918
  };
919
+ this.streamContinuationGeneration = this.continuationGeneration;
903
920
  this.setIsLoading(true);
904
921
  this.setStatus("streaming");
905
922
  (async () => {
@@ -1027,8 +1044,12 @@ var ChatClient = class {
1027
1044
  * - A MultimodalContent object with content array and optional custom ID
1028
1045
  * @param body - Optional body parameters to merge with the client's base body for this request.
1029
1046
  * Uses shallow merge with per-message body taking priority.
1030
- * @param sendOptions - Per-call overrides, e.g. `{ whenBusy: 'interrupt' }` to
1031
- * override the configured queue policy for this one send.
1047
+ * @param sendOptions - Per-call overrides. `{ whenBusy }` overrides the
1048
+ * queue policy for this one send. `{ body }`
1049
+ * shallow-merges with `body` and with the chat-level
1050
+ * `body` / `forwardedProps`. `sendOptions.body` wins
1051
+ * on key collisions. Framework hooks forward this
1052
+ * object as their second argument.
1032
1053
  *
1033
1054
  * @example
1034
1055
  * ```ts
@@ -1038,9 +1059,13 @@ var ChatClient = class {
1038
1059
  * // Text message with custom body params
1039
1060
  * await client.sendMessage('Hello!', { temperature: 0.7 })
1040
1061
  *
1041
- * // Per-call whenBusy override (body must still be the 2nd arg on ChatClient)
1062
+ * // Per-call whenBusy override
1042
1063
  * await client.sendMessage('Urgent', undefined, { whenBusy: 'interrupt' })
1043
1064
  *
1065
+ * // Per-call body via options. Same effect as the positional arg.
1066
+ * // This is the shape the framework hooks (`useChat`, `injectChat`) forward.
1067
+ * await client.sendMessage('Hello!', undefined, { body: { temperature: 0.7 } })
1068
+ *
1044
1069
  * // Multimodal message with image
1045
1070
  * await client.sendMessage({
1046
1071
  * content: [
@@ -1066,11 +1091,15 @@ var ChatClient = class {
1066
1091
  this.mountDevtools();
1067
1092
  if (typeof content === "string" && !content.trim()) return;
1068
1093
  if (this.hasBlockingInterrupts()) throw new Error("ChatClient: cannot send normal input while pending interrupts exist. Use resumeInterrupts() instead.");
1094
+ const resolvedBody = {
1095
+ ...body,
1096
+ ...sendOptions?.body
1097
+ };
1069
1098
  if (this.isSendBusy()) {
1070
1099
  const { action, id } = this.decideWhenBusy(content, sendOptions);
1071
1100
  if (action === "drop") return;
1072
1101
  if (action === "queue") {
1073
- this.enqueueMessage(content, body, id);
1102
+ this.enqueueMessage(content, resolvedBody, id);
1074
1103
  return;
1075
1104
  }
1076
1105
  this.stopMessageQueueDrain = true;
@@ -1079,7 +1108,7 @@ var ChatClient = class {
1079
1108
  this.resetSessionGenerating();
1080
1109
  } else this.sendInFlight = true;
1081
1110
  try {
1082
- await this.deliverMessage(content, body);
1111
+ await this.deliverMessage(content, resolvedBody);
1083
1112
  } finally {
1084
1113
  this.sendInFlight = false;
1085
1114
  }
@@ -1206,6 +1235,7 @@ var ChatClient = class {
1206
1235
  async streamResponse() {
1207
1236
  if (this.isLoading) return false;
1208
1237
  const generation = ++this.streamGeneration;
1238
+ this.streamContinuationGeneration = this.continuationGeneration;
1209
1239
  const resumeThreadId = this.pendingResumeThreadId;
1210
1240
  const resumeParentRunId = this.pendingResumeParentRunId;
1211
1241
  const resumeItems = this.pendingResumeItems;
@@ -1247,6 +1277,11 @@ var ChatClient = class {
1247
1277
  this.processor.prepareAssistantMessage();
1248
1278
  this.ensureSubscription();
1249
1279
  const processingComplete = this.waitForProcessing();
1280
+ let byokHeaders;
1281
+ if (this.byok) {
1282
+ const provider = resolveByokProviderId(this.byokProvider, mergedBody.provider);
1283
+ byokHeaders = await prepareResolvedByokHeaders(this.byok, provider);
1284
+ }
1250
1285
  const runContext = {
1251
1286
  threadId: resumeThreadId ?? this.threadId,
1252
1287
  runId,
@@ -1257,7 +1292,8 @@ var ChatClient = class {
1257
1292
  parameters: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : { type: "object" }
1258
1293
  })),
1259
1294
  forwardedProps: { ...mergedBody },
1260
- ...resumeItems ? { resume: resumeItems } : {}
1295
+ ...resumeItems ? { resume: resumeItems } : {},
1296
+ ...byokHeaders ? { headers: byokHeaders } : {}
1261
1297
  };
1262
1298
  this.devtoolsBridge.beginRun(runContext.runId, runContext.threadId);
1263
1299
  activeDevtoolsRunId = runContext.runId;
@@ -1279,22 +1315,24 @@ var ChatClient = class {
1279
1315
  this.processor.finalizeStream();
1280
1316
  streamCompletedSuccessfully = true;
1281
1317
  } catch (err) {
1282
- if (err instanceof Error) {
1283
- if (err.name === "AbortError") {
1284
- if (activeDevtoolsRunId) {
1285
- this.devtoolsBridge.emitRunLifecycle("run:cancelled", activeDevtoolsRunId, "cancelled");
1286
- runTerminalEventEmitted = true;
1287
- }
1288
- return false;
1318
+ const error = err instanceof Error ? err : new Error(String(err));
1319
+ if (error.name === "AbortError") {
1320
+ if (activeDevtoolsRunId) {
1321
+ this.devtoolsBridge.emitRunLifecycle("run:cancelled", activeDevtoolsRunId, "cancelled");
1322
+ runTerminalEventEmitted = true;
1289
1323
  }
1290
- if (generation === this.streamGeneration) {
1291
- this.reportStreamError(err);
1292
- if (activeDevtoolsRunId) {
1293
- this.devtoolsBridge.emitRunLifecycle("run:errored", activeDevtoolsRunId, "errored", { error: err.message });
1294
- runTerminalEventEmitted = true;
1295
- }
1324
+ return false;
1325
+ }
1326
+ if (error instanceof ByokMissingError) this.byok?.request(error.provider, "missing");
1327
+ if (error instanceof ByokBlockedError && error.reason === "locked") this.byok?.request(error.provider, "locked");
1328
+ if (generation === this.streamGeneration) {
1329
+ this.reportStreamError(error);
1330
+ if (activeDevtoolsRunId) {
1331
+ this.devtoolsBridge.emitRunLifecycle("run:errored", activeDevtoolsRunId, "errored", { error: error.message });
1332
+ runTerminalEventEmitted = true;
1296
1333
  }
1297
1334
  }
1335
+ if (generation === this.streamGeneration && (error instanceof ByokMissingError || error instanceof ByokBlockedError || error instanceof ByokUnresolvedProviderError)) throw error;
1298
1336
  } finally {
1299
1337
  if (generation === this.streamGeneration) {
1300
1338
  this.currentStreamId = null;
@@ -1312,18 +1350,8 @@ var ChatClient = class {
1312
1350
  }
1313
1351
  await this.drainPostStreamActions();
1314
1352
  if (streamCompletedSuccessfully) {
1315
- const lastPart = this.processor.getMessages().at(-1)?.parts.at(-1);
1316
- const { finishReason } = this.processor.getState();
1317
- if (lastPart?.type === "tool-result" && finishReason !== "stop" && this.shouldAutoSend()) try {
1318
- await this.checkForContinuation();
1319
- } catch (error) {
1320
- console.error("Failed to continue flow after tool result:", error);
1321
- await this.drainQueue();
1322
- }
1323
- else {
1324
- if (this.status !== "ready") this.setStatus("ready");
1325
- if (!this.messageQueueDraining) await this.drainQueue();
1326
- }
1353
+ if (this.status !== "ready") this.setStatus("ready");
1354
+ if (!this.messageQueueDraining) await this.drainQueue();
1327
1355
  } else this.flushQueue();
1328
1356
  }
1329
1357
  }
@@ -1374,9 +1402,13 @@ var ChatClient = class {
1374
1402
  * Stop the current stream
1375
1403
  */
1376
1404
  stop() {
1405
+ this.continuationGeneration++;
1377
1406
  const hadLocalStream = this.abortController !== null;
1378
1407
  this.cancelInFlightStream({ setReadyStatus: true });
1379
1408
  this.discardPendingSends();
1409
+ this.lastResume = null;
1410
+ this.activeInterruptSubmission = void 0;
1411
+ this.interruptManager.reset();
1380
1412
  if (hadLocalStream) this.resetSessionGenerating();
1381
1413
  this.events.stopped();
1382
1414
  }
@@ -1411,9 +1443,9 @@ var ChatClient = class {
1411
1443
  */
1412
1444
  async addToolResult(result) {
1413
1445
  const clientTool = this.clientToolsRef.current.get(result.tool);
1414
- await this.addToolResultForClientTool(result, clientTool);
1446
+ await this.addToolResultForClientTool(result, clientTool, this.streamContinuationGeneration);
1415
1447
  }
1416
- async addToolResultForClientTool(result, clientTool, context) {
1448
+ async addToolResultForClientTool(result, clientTool, continuationGeneration, context) {
1417
1449
  if (clientTool && result.state !== "output-error") try {
1418
1450
  result = {
1419
1451
  ...result,
@@ -1428,11 +1460,12 @@ var ChatClient = class {
1428
1460
  };
1429
1461
  }
1430
1462
  this.events.toolResultAdded(result.toolCallId, result.tool, result.output, result.state || "output-available", context);
1463
+ if (continuationGeneration !== this.continuationGeneration) return;
1431
1464
  this.processor.addToolResult(result.toolCallId, result.output, result.state === "output-error" ? result.errorText || "Tool execution failed" : void 0);
1432
1465
  this.devtoolsBridge.emitSnapshot();
1433
1466
  if (this.interruptManager.resolveClientToolOutput(result.toolCallId, result.state === "output-error" ? { error: result.errorText || "Tool execution failed" } : result.output)) return;
1434
1467
  if (this.isLoading) {
1435
- this.queuePostStreamAction(() => this.checkForContinuation());
1468
+ this.queuePostStreamAction(() => continuationGeneration === this.continuationGeneration ? this.checkForContinuation() : Promise.resolve());
1436
1469
  return;
1437
1470
  }
1438
1471
  await this.checkForContinuation();
@@ -1470,7 +1503,11 @@ var ChatClient = class {
1470
1503
  * Queue an action to be executed after the current stream ends
1471
1504
  */
1472
1505
  queuePostStreamAction(action) {
1473
- this.postStreamActions.push(action);
1506
+ const continuationGeneration = this.continuationGeneration;
1507
+ this.postStreamActions.push(async () => {
1508
+ if (continuationGeneration !== this.continuationGeneration) return;
1509
+ await action();
1510
+ });
1474
1511
  }
1475
1512
  /**
1476
1513
  * Drain and execute all queued post-stream actions
@@ -1489,6 +1526,7 @@ var ChatClient = class {
1489
1526
  * Check if we should continue the flow and do so if needed
1490
1527
  */
1491
1528
  async checkForContinuation() {
1529
+ if (this.streamContinuationGeneration !== this.continuationGeneration) return;
1492
1530
  if (this.hasPendingInterrupts()) return;
1493
1531
  if (this.continuationPending || this.isLoading) {
1494
1532
  this.continuationSkipped = true;
@@ -1680,6 +1718,8 @@ var ChatClient = class {
1680
1718
  }
1681
1719
  if (options.body !== void 0) this.bodyOption = options.body;
1682
1720
  if (options.forwardedProps !== void 0) this.forwardedPropsOption = options.forwardedProps;
1721
+ if (options.byok !== void 0) this.byok = options.byok;
1722
+ if (options.byokProvider !== void 0) this.byokProvider = options.byokProvider;
1683
1723
  if ("context" in options) this.context = options.context;
1684
1724
  if (options.tools !== void 0) {
1685
1725
  this.interruptManager.updateTools(options.tools);