@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.
- package/README.md +2 -1
- package/dist/esm/byok/client.d.ts +52 -0
- package/dist/esm/byok/client.js +300 -0
- package/dist/esm/byok/client.js.map +1 -0
- package/dist/esm/byok/passkey.d.ts +43 -0
- package/dist/esm/byok/passkey.js +251 -0
- package/dist/esm/byok/passkey.js.map +1 -0
- package/dist/esm/byok/resolve.d.ts +8 -0
- package/dist/esm/byok/resolve.js +20 -0
- package/dist/esm/byok/resolve.js.map +1 -0
- package/dist/esm/byok/storage.d.ts +15 -0
- package/dist/esm/byok/storage.js +16 -0
- package/dist/esm/byok/storage.js.map +1 -0
- package/dist/esm/byok.d.ts +8 -0
- package/dist/esm/byok.js +5 -0
- package/dist/esm/chat-client.d.ts +18 -5
- package/dist/esm/chat-client.js +96 -59
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +70 -30
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +27 -6
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- package/dist/esm/generation-types.js +4 -2
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/interrupt-manager.d.ts +1 -0
- package/dist/esm/interrupt-manager.js +15 -6
- package/dist/esm/interrupt-manager.js.map +1 -1
- package/dist/esm/sse-parser.js +8 -1
- package/dist/esm/sse-parser.js.map +1 -1
- package/dist/esm/types.d.ts +46 -3
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +3 -1
- package/dist/esm/video-generation-client.js +29 -8
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +7 -3
- package/src/byok/client.ts +360 -0
- package/src/byok/passkey.ts +399 -0
- package/src/byok/resolve.ts +30 -0
- package/src/byok/storage.ts +29 -0
- package/src/byok.ts +20 -0
- package/src/chat-client.ts +138 -77
- package/src/connection-adapters.ts +108 -53
- package/src/generation-client.ts +53 -9
- package/src/generation-types.ts +24 -2
- package/src/interrupt-manager.ts +20 -6
- package/src/sse-parser.ts +10 -0
- package/src/types.ts +48 -3
- 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
|
+
}
|