@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,360 @@
1
+ import {
2
+ ByokBlockedError,
3
+ byokHeaderName,
4
+ isProviderId,
5
+ maskKey,
6
+ } from '@tanstack/ai/byok'
7
+ import { memoryStorage } from './storage'
8
+ import type { ProviderId } from '@tanstack/ai/byok'
9
+ import type { Keyring, KeyringStorage } from './storage'
10
+
11
+ export type KeyStatus =
12
+ | { state: 'empty' }
13
+ | { state: 'set'; masked: string }
14
+ | { state: 'locked'; masked: string }
15
+ | { state: 'error'; masked: string; message: string }
16
+
17
+ export type ByokPrompt = {
18
+ provider: ProviderId
19
+ reason: 'missing' | 'locked'
20
+ }
21
+
22
+ export type ByokSnapshot = {
23
+ status: Partial<Record<string, KeyStatus>>
24
+ locked: boolean
25
+ prompt: ByokPrompt | null
26
+ storageError: string | null
27
+ }
28
+
29
+ export interface DefineByokOptions {
30
+ storage?: KeyringStorage
31
+ }
32
+
33
+ export const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {
34
+ status: {},
35
+ locked: false,
36
+ prompt: null,
37
+ storageError: null,
38
+ }
39
+
40
+ function requireProviderId(value: string): ProviderId {
41
+ if (!isProviderId(value)) {
42
+ throw new Error(`Invalid BYOK provider id: ${value}`)
43
+ }
44
+ return value
45
+ }
46
+
47
+ function errorMessage(error: unknown, fallback: string): string {
48
+ return error instanceof Error && error.message.length > 0
49
+ ? error.message
50
+ : fallback
51
+ }
52
+
53
+ function isByokCeremonyCancel(error: unknown): boolean {
54
+ if (
55
+ typeof DOMException !== 'undefined' &&
56
+ error instanceof DOMException &&
57
+ error.name === 'NotAllowedError'
58
+ ) {
59
+ return true
60
+ }
61
+ if (error instanceof Error) {
62
+ if (error.name === 'NotAllowedError') return true
63
+ const message = error.message.toLowerCase()
64
+ return message.includes('cancelled') || message.includes('not allowed')
65
+ }
66
+ return false
67
+ }
68
+
69
+ function sanitizeKeyring(value: unknown): Keyring {
70
+ if (typeof value !== 'object' || value === null) return {}
71
+ const keys: Keyring = {}
72
+ for (const [provider, key] of Object.entries(value)) {
73
+ if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {
74
+ keys[provider] = key
75
+ }
76
+ }
77
+ return keys
78
+ }
79
+
80
+ export class ByokClient {
81
+ readonly storage: KeyringStorage
82
+ #keys: Keyring = {}
83
+ #statuses: Partial<Record<string, KeyStatus>> = {}
84
+ #locked: boolean
85
+ #prompt: ByokPrompt | null = null
86
+ #coverageAll = false
87
+ #coverage: Record<string, boolean> = {}
88
+ readonly #listeners = new Set<() => void>()
89
+ #snapshot: ByokSnapshot
90
+ #storageError: string | null = null
91
+ readonly #ready: Promise<void>
92
+
93
+ constructor(options: DefineByokOptions = {}) {
94
+ this.storage = options.storage ?? memoryStorage()
95
+ this.#locked = Boolean(this.storage.unlockable)
96
+ this.#snapshot = this.#buildSnapshot()
97
+ this.#ready = this.#hydrate()
98
+ }
99
+
100
+ /** Resolves when constructor hydration (peek/load) finishes. */
101
+ ready = (): Promise<void> => this.#ready
102
+
103
+ subscribe = (listener: () => void): (() => void) => {
104
+ this.#listeners.add(listener)
105
+ return () => {
106
+ this.#listeners.delete(listener)
107
+ }
108
+ }
109
+
110
+ getSnapshot = (): ByokSnapshot => this.#snapshot
111
+
112
+ #buildSnapshot(): ByokSnapshot {
113
+ return {
114
+ status: { ...this.#statuses },
115
+ locked: this.#locked,
116
+ prompt: this.#prompt,
117
+ storageError: this.#storageError,
118
+ }
119
+ }
120
+
121
+ keys(): Keyring {
122
+ return { ...this.#keys }
123
+ }
124
+
125
+ request(provider: ProviderId, reason: ByokPrompt['reason']): void {
126
+ this.#prompt = { provider: requireProviderId(provider), reason }
127
+ this.#emit()
128
+ }
129
+
130
+ /**
131
+ * `true` — the server can fill any slug from env.
132
+ * `false` — no env coverage.
133
+ * A record merges per-id flags and clears the all/none boolean.
134
+ */
135
+ setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void {
136
+ if (typeof flags === 'boolean') {
137
+ this.#coverageAll = flags
138
+ this.#coverage = {}
139
+ return
140
+ }
141
+ this.#coverageAll = false
142
+ this.#coverage = { ...this.#coverage, ...flags }
143
+ }
144
+
145
+ #hasCoverage(provider: string): boolean {
146
+ return this.#coverageAll || this.#coverage[provider] === true
147
+ }
148
+
149
+ headers(provider?: ProviderId): Record<string, string> {
150
+ const headers: Record<string, string> = {}
151
+ if (provider) {
152
+ requireProviderId(provider)
153
+ const key = this.#keys[provider]
154
+ if (key) headers[byokHeaderName(provider)] = key
155
+ return headers
156
+ }
157
+ for (const [id, key] of Object.entries(this.#keys)) {
158
+ if (key) headers[byokHeaderName(id)] = key
159
+ }
160
+ return headers
161
+ }
162
+
163
+ async prepare(provider?: ProviderId): Promise<void> {
164
+ await this.#ready
165
+ if (this.storage.unlockable && this.#locked) {
166
+ await this.unlock()
167
+ }
168
+ if (!provider) return
169
+ requireProviderId(provider)
170
+ if (this.#keys[provider]) return
171
+ if (this.#hasCoverage(provider)) return
172
+ this.request(provider, 'missing')
173
+ throw new ByokBlockedError(provider, 'missing')
174
+ }
175
+
176
+ async update(
177
+ providerOrKey: ProviderId | string,
178
+ key?: string,
179
+ ): Promise<void> {
180
+ await this.#ready
181
+ let provider: ProviderId
182
+ let nextKey: string
183
+ if (key === undefined) {
184
+ if (!this.#prompt) {
185
+ throw new Error('byok.update(key) cannot run when prompt is null')
186
+ }
187
+ provider = this.#prompt.provider
188
+ nextKey = providerOrKey
189
+ } else {
190
+ provider = requireProviderId(providerOrKey)
191
+ nextKey = key
192
+ }
193
+ nextKey = nextKey.trim()
194
+ if (nextKey.length === 0) {
195
+ throw new Error('BYOK key must be non-empty')
196
+ }
197
+ if (this.storage.unlockable && this.#locked) {
198
+ await this.unlock()
199
+ }
200
+ const previousKeys = this.#keys
201
+ const previousStatuses = { ...this.#statuses }
202
+ const previousLocked = this.#locked
203
+ const previousPrompt = this.#prompt
204
+ const previousStorageError = this.#storageError
205
+ const next = { ...this.#keys, [provider]: nextKey }
206
+ try {
207
+ await this.storage.save(next)
208
+ } catch (error) {
209
+ this.#keys = previousKeys
210
+ this.#statuses = previousStatuses
211
+ this.#locked = previousLocked
212
+ this.#prompt = previousPrompt
213
+ this.#storageError = previousStorageError
214
+ this.#statuses[provider] = {
215
+ state: 'error',
216
+ masked: maskKey(nextKey),
217
+ message: errorMessage(error, `Failed to persist ${provider} key`),
218
+ }
219
+ this.#emit()
220
+ throw error
221
+ }
222
+ this.#keys = next
223
+ this.#statuses[provider] = { state: 'set', masked: maskKey(nextKey) }
224
+ this.#locked = false
225
+ this.#prompt = null
226
+ this.#storageError = null
227
+ this.#emit()
228
+ }
229
+
230
+ async clear(provider?: ProviderId): Promise<void> {
231
+ await this.#ready
232
+ if (provider) {
233
+ requireProviderId(provider)
234
+ if (this.storage.unlockable && this.#locked) {
235
+ await this.unlock()
236
+ }
237
+ const previousKeys = this.#keys
238
+ const previousStatuses = { ...this.#statuses }
239
+ const next = { ...this.#keys }
240
+ delete next[provider]
241
+ try {
242
+ await this.storage.save(next)
243
+ } catch (error) {
244
+ this.#keys = previousKeys
245
+ this.#statuses = previousStatuses
246
+ this.#emit()
247
+ throw error
248
+ }
249
+ this.#keys = next
250
+ delete this.#statuses[provider]
251
+ this.#emit()
252
+ return
253
+ }
254
+ const previousKeys = this.#keys
255
+ const previousStatuses = this.#statuses
256
+ const previousLocked = this.#locked
257
+ const previousPrompt = this.#prompt
258
+ try {
259
+ await this.storage.clear()
260
+ } catch (error) {
261
+ this.#keys = previousKeys
262
+ this.#statuses = previousStatuses
263
+ this.#locked = previousLocked
264
+ this.#prompt = previousPrompt
265
+ this.#emit()
266
+ throw error
267
+ }
268
+ this.#keys = {}
269
+ this.#statuses = {}
270
+ this.#locked = false
271
+ this.#prompt = null
272
+ this.#storageError = null
273
+ this.#emit()
274
+ }
275
+
276
+ async unlock(): Promise<void> {
277
+ await this.#ready
278
+ if (!this.storage.unlockable) return
279
+ try {
280
+ const loaded = sanitizeKeyring(await this.storage.load())
281
+ this.#keys = { ...loaded, ...this.#keys }
282
+ for (const [id, value] of Object.entries(loaded)) {
283
+ if (!isProviderId(id) || !value) continue
284
+ const existing = this.#statuses[id]
285
+ if (!existing || existing.state === 'locked') {
286
+ this.#statuses[id] = { state: 'set', masked: maskKey(value) }
287
+ }
288
+ }
289
+ this.#locked = false
290
+ this.#storageError = null
291
+ this.#emit()
292
+ } catch (error) {
293
+ if (isByokCeremonyCancel(error)) {
294
+ const provider = this.#lockedProvider()
295
+ if (provider) {
296
+ this.request(provider, 'locked')
297
+ throw new ByokBlockedError(provider, 'locked')
298
+ }
299
+ }
300
+ this.#storageError = errorMessage(error, 'Failed to unlock keyring')
301
+ this.#emit()
302
+ throw error instanceof Error ? error : new Error(String(error))
303
+ }
304
+ }
305
+
306
+ #lockedProvider(): ProviderId | undefined {
307
+ if (this.#prompt && isProviderId(this.#prompt.provider)) {
308
+ return this.#prompt.provider
309
+ }
310
+ for (const [id, status] of Object.entries(this.#statuses)) {
311
+ if (status?.state === 'locked' && isProviderId(id)) return id
312
+ }
313
+ return undefined
314
+ }
315
+
316
+ async #hydrate(): Promise<void> {
317
+ if (this.storage.unlockable) {
318
+ if (!this.storage.peek) return
319
+ try {
320
+ const preview = await this.storage.peek()
321
+ for (const [id, last4] of Object.entries(preview)) {
322
+ if (!isProviderId(id) || this.#statuses[id]) continue
323
+ this.#statuses[id] = {
324
+ state: 'locked',
325
+ masked: last4 ? last4 : '••',
326
+ }
327
+ }
328
+ this.#locked = Object.keys(preview).length > 0
329
+ this.#storageError = null
330
+ this.#emit()
331
+ } catch (error) {
332
+ this.#storageError = errorMessage(error, 'Failed to read keyring')
333
+ this.#emit()
334
+ }
335
+ return
336
+ }
337
+ try {
338
+ const loaded = sanitizeKeyring(await this.storage.load())
339
+ this.#keys = { ...loaded, ...this.#keys }
340
+ for (const [id, value] of Object.entries(loaded)) {
341
+ if (!isProviderId(id) || !value) continue
342
+ this.#statuses[id] = { state: 'set', masked: maskKey(value) }
343
+ }
344
+ this.#storageError = null
345
+ this.#emit()
346
+ } catch (error) {
347
+ this.#storageError = errorMessage(error, 'Failed to read keyring')
348
+ this.#emit()
349
+ }
350
+ }
351
+
352
+ #emit(): void {
353
+ this.#snapshot = this.#buildSnapshot()
354
+ for (const listener of this.#listeners) listener()
355
+ }
356
+ }
357
+
358
+ export function defineByok(options: DefineByokOptions = {}): ByokClient {
359
+ return new ByokClient(options)
360
+ }