@tanstack/ai-client 0.26.0 → 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/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 +16 -3
- package/dist/esm/chat-client.js +49 -31
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +2 -0
- package/dist/esm/connection-adapters.js +33 -9
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/generation-client.d.ts +3 -1
- package/dist/esm/generation-client.js +23 -4
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +14 -0
- 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 +29 -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 +25 -6
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +6 -2
- 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 +95 -65
- package/src/connection-adapters.ts +41 -12
- package/src/generation-client.ts +48 -4
- package/src/generation-types.ts +16 -0
- package/src/interrupt-manager.ts +20 -6
- package/src/sse-parser.ts +10 -0
- package/src/types.ts +31 -3
- package/src/video-generation-client.ts +45 -4
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { isProviderId } from '@tanstack/ai/byok'
|
|
2
|
+
import { memoryStorage } from './storage'
|
|
3
|
+
import type { KeyPreview, Keyring, KeyringStorage } from './storage'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).
|
|
7
|
+
*
|
|
8
|
+
* The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived
|
|
9
|
+
* from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.
|
|
10
|
+
* Decryption happens entirely client-side with the user present — no server,
|
|
11
|
+
* no custodian.
|
|
12
|
+
*
|
|
13
|
+
* Honest scope: this protects against at-rest theft (stolen device,
|
|
14
|
+
* storage-dumping extension, backups). It does NOT defeat live in-page XSS —
|
|
15
|
+
* an attacker running JS in the origin after the user unlocks can read the
|
|
16
|
+
* decrypted keys from memory.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const STORE_NAME = 'keyring'
|
|
20
|
+
const RECORD_ID = 'default'
|
|
21
|
+
const HKDF_INFO = 'byok:keyring:v1'
|
|
22
|
+
const DEFAULT_DB = 'byok'
|
|
23
|
+
|
|
24
|
+
interface StoredRecord {
|
|
25
|
+
id: string
|
|
26
|
+
/** The passkey's raw credential id, replayed in the unlock ceremony. */
|
|
27
|
+
credentialId: ArrayBuffer
|
|
28
|
+
/** Fixed per-install PRF evaluation input (not secret). */
|
|
29
|
+
salt: ArrayBuffer
|
|
30
|
+
/** AES-GCM initialization vector for this ciphertext. */
|
|
31
|
+
iv: ArrayBuffer
|
|
32
|
+
/** Encrypted keyring JSON. */
|
|
33
|
+
ciphertext: ArrayBuffer
|
|
34
|
+
/**
|
|
35
|
+
* Unencrypted presence metadata (`provider → last 4`). Non-sensitive, so it
|
|
36
|
+
* can be read via {@link KeyringStorage.peek} without an unlock ceremony to
|
|
37
|
+
* show saved keys as "locked" after a refresh.
|
|
38
|
+
*/
|
|
39
|
+
preview: KeyPreview
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sanitizeKeyring(value: unknown): Keyring {
|
|
43
|
+
if (typeof value !== 'object' || value === null) return {}
|
|
44
|
+
const keys: Keyring = {}
|
|
45
|
+
for (const [provider, key] of Object.entries(value)) {
|
|
46
|
+
if (isProviderId(provider) && typeof key === 'string' && key.length > 0) {
|
|
47
|
+
keys[provider] = key
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return keys
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Build the non-sensitive `provider → last 4` preview from a keyring. */
|
|
54
|
+
function previewOf(keys: Keyring): KeyPreview {
|
|
55
|
+
const preview: KeyPreview = {}
|
|
56
|
+
for (const [provider, key] of Object.entries(keys)) {
|
|
57
|
+
if (!key || !isProviderId(provider)) continue
|
|
58
|
+
// Keys of length ≤ 4 would make last-4 the whole secret — store presence only.
|
|
59
|
+
preview[provider] = key.length > 4 ? key.slice(-4) : ''
|
|
60
|
+
}
|
|
61
|
+
return preview
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether the current environment exposes WebAuthn. Actual PRF support can
|
|
66
|
+
* only be confirmed during registration; `passkeyStorage` throws if the
|
|
67
|
+
* chosen authenticator does not support PRF.
|
|
68
|
+
*/
|
|
69
|
+
export function isPasskeyStorageSupported(): boolean {
|
|
70
|
+
return (
|
|
71
|
+
typeof globalThis !== 'undefined' &&
|
|
72
|
+
typeof globalThis.PublicKeyCredential !== 'undefined' &&
|
|
73
|
+
typeof globalThis.navigator !== 'undefined' &&
|
|
74
|
+
typeof globalThis.navigator.credentials.create === 'function'
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Crypto (exported for testing; the WebAuthn ceremony below feeds `deriveAesKey`)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */
|
|
83
|
+
export async function deriveAesKey(
|
|
84
|
+
prfOutput: BufferSource,
|
|
85
|
+
): Promise<CryptoKey> {
|
|
86
|
+
const base = await crypto.subtle.importKey('raw', prfOutput, 'HKDF', false, [
|
|
87
|
+
'deriveKey',
|
|
88
|
+
])
|
|
89
|
+
return crypto.subtle.deriveKey(
|
|
90
|
+
{
|
|
91
|
+
name: 'HKDF',
|
|
92
|
+
hash: 'SHA-256',
|
|
93
|
+
salt: new Uint8Array(0),
|
|
94
|
+
info: new TextEncoder().encode(HKDF_INFO),
|
|
95
|
+
},
|
|
96
|
+
base,
|
|
97
|
+
{ name: 'AES-GCM', length: 256 },
|
|
98
|
+
false,
|
|
99
|
+
['encrypt', 'decrypt'],
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function encryptKeyring(
|
|
104
|
+
key: CryptoKey,
|
|
105
|
+
keys: Keyring,
|
|
106
|
+
): Promise<{ iv: ArrayBuffer; ciphertext: ArrayBuffer }> {
|
|
107
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
108
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(keys))
|
|
109
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
110
|
+
{ name: 'AES-GCM', iv },
|
|
111
|
+
key,
|
|
112
|
+
plaintext,
|
|
113
|
+
)
|
|
114
|
+
return { iv: iv.buffer, ciphertext }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function decryptKeyring(
|
|
118
|
+
key: CryptoKey,
|
|
119
|
+
iv: BufferSource,
|
|
120
|
+
ciphertext: BufferSource,
|
|
121
|
+
): Promise<Keyring> {
|
|
122
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
123
|
+
{ name: 'AES-GCM', iv },
|
|
124
|
+
key,
|
|
125
|
+
ciphertext,
|
|
126
|
+
)
|
|
127
|
+
const parsed: unknown = JSON.parse(new TextDecoder().decode(plaintext))
|
|
128
|
+
return sanitizeKeyring(parsed)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// IndexedDB
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function openDb(dbName: string): Promise<IDBDatabase> {
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const request = indexedDB.open(dbName, 1)
|
|
138
|
+
request.onupgradeneeded = () => {
|
|
139
|
+
request.result.createObjectStore(STORE_NAME, { keyPath: 'id' })
|
|
140
|
+
}
|
|
141
|
+
request.onsuccess = () => resolve(request.result)
|
|
142
|
+
request.onerror = () => reject(request.error)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function idbGet(dbName: string): Promise<StoredRecord | null> {
|
|
147
|
+
return openDb(dbName).then(
|
|
148
|
+
(db) =>
|
|
149
|
+
new Promise((resolve, reject) => {
|
|
150
|
+
const request = db
|
|
151
|
+
.transaction(STORE_NAME, 'readonly')
|
|
152
|
+
.objectStore(STORE_NAME)
|
|
153
|
+
.get(RECORD_ID)
|
|
154
|
+
request.onsuccess = () => resolve(request.result ?? null)
|
|
155
|
+
request.onerror = () => reject(request.error)
|
|
156
|
+
}),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function idbPut(dbName: string, record: StoredRecord): Promise<void> {
|
|
161
|
+
return openDb(dbName).then(
|
|
162
|
+
(db) =>
|
|
163
|
+
new Promise((resolve, reject) => {
|
|
164
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
165
|
+
tx.objectStore(STORE_NAME).put(record)
|
|
166
|
+
tx.oncomplete = () => resolve()
|
|
167
|
+
tx.onerror = () => reject(tx.error)
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function idbClear(dbName: string): Promise<void> {
|
|
173
|
+
return openDb(dbName).then(
|
|
174
|
+
(db) =>
|
|
175
|
+
new Promise((resolve, reject) => {
|
|
176
|
+
const tx = db.transaction(STORE_NAME, 'readwrite')
|
|
177
|
+
tx.objectStore(STORE_NAME).delete(RECORD_ID)
|
|
178
|
+
tx.oncomplete = () => resolve()
|
|
179
|
+
tx.onerror = () => reject(tx.error)
|
|
180
|
+
}),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// WebAuthn ceremonies
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
function requirePublicKeyCredential(
|
|
189
|
+
credential: Credential | null,
|
|
190
|
+
action: string,
|
|
191
|
+
): PublicKeyCredential {
|
|
192
|
+
if (!credential) throw new Error(`Passkey ${action} was cancelled`)
|
|
193
|
+
if (!(credential instanceof PublicKeyCredential)) {
|
|
194
|
+
throw new Error(`Unexpected credential type during ${action}`)
|
|
195
|
+
}
|
|
196
|
+
return credential
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function registerPasskey(
|
|
200
|
+
rpName: string,
|
|
201
|
+
userName: string,
|
|
202
|
+
rpId?: string,
|
|
203
|
+
): Promise<{
|
|
204
|
+
credentialId: ArrayBuffer
|
|
205
|
+
salt: Uint8Array<ArrayBuffer>
|
|
206
|
+
prf?: BufferSource
|
|
207
|
+
}> {
|
|
208
|
+
const salt = crypto.getRandomValues(new Uint8Array(32))
|
|
209
|
+
const credential = requirePublicKeyCredential(
|
|
210
|
+
await navigator.credentials.create({
|
|
211
|
+
publicKey: {
|
|
212
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
213
|
+
// Omit `id` to let the browser bind the passkey to the current origin's
|
|
214
|
+
// effective domain; set it to scope across subdomains of a self-host.
|
|
215
|
+
rp: rpId ? { name: rpName, id: rpId } : { name: rpName },
|
|
216
|
+
user: {
|
|
217
|
+
id: crypto.getRandomValues(new Uint8Array(16)),
|
|
218
|
+
name: userName,
|
|
219
|
+
displayName: userName,
|
|
220
|
+
},
|
|
221
|
+
pubKeyCredParams: [
|
|
222
|
+
{ type: 'public-key', alg: -7 },
|
|
223
|
+
{ type: 'public-key', alg: -257 },
|
|
224
|
+
],
|
|
225
|
+
authenticatorSelection: {
|
|
226
|
+
residentKey: 'required',
|
|
227
|
+
userVerification: 'required',
|
|
228
|
+
},
|
|
229
|
+
extensions: { prf: { eval: { first: salt } } },
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
'registration',
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const prf = credential.getClientExtensionResults().prf
|
|
236
|
+
if (!prf?.enabled) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'This authenticator does not support the WebAuthn PRF extension',
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return { credentialId: credential.rawId, salt, prf: prf.results?.first }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function evaluatePrf(
|
|
245
|
+
credentialId: BufferSource,
|
|
246
|
+
salt: BufferSource,
|
|
247
|
+
): Promise<BufferSource> {
|
|
248
|
+
const credential = requirePublicKeyCredential(
|
|
249
|
+
await navigator.credentials.get({
|
|
250
|
+
publicKey: {
|
|
251
|
+
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
252
|
+
allowCredentials: [{ type: 'public-key', id: credentialId }],
|
|
253
|
+
userVerification: 'required',
|
|
254
|
+
extensions: { prf: { eval: { first: salt } } },
|
|
255
|
+
},
|
|
256
|
+
}),
|
|
257
|
+
'unlock',
|
|
258
|
+
)
|
|
259
|
+
const result = credential.getClientExtensionResults().prf?.results?.first
|
|
260
|
+
if (!result) {
|
|
261
|
+
throw new Error('Authenticator did not return a PRF result')
|
|
262
|
+
}
|
|
263
|
+
return result
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// Storage strategy
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
export interface PasskeyStorageOptions {
|
|
271
|
+
/** Relying-party name shown in the passkey prompt. */
|
|
272
|
+
rpName?: string
|
|
273
|
+
/** Username label attached to the created passkey. */
|
|
274
|
+
userName?: string
|
|
275
|
+
/**
|
|
276
|
+
* WebAuthn Relying Party ID. Omit to bind the passkey to the current origin's
|
|
277
|
+
* effective domain (the default — no central/hardcoded domain). Set it to a
|
|
278
|
+
* registrable parent domain to share the credential across subdomains of your
|
|
279
|
+
* own deployment. The encrypted keyring is never portable across unrelated
|
|
280
|
+
* domains.
|
|
281
|
+
*/
|
|
282
|
+
rpId?: string
|
|
283
|
+
/** IndexedDB database name. Defaults to `byok`. */
|
|
284
|
+
dbName?: string
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so
|
|
289
|
+
* nothing is decrypted until the user calls `unlock()` (or saves a key, which
|
|
290
|
+
* registers a passkey on first use). The derived key is cached in memory for
|
|
291
|
+
* the session so repeated saves don't re-prompt.
|
|
292
|
+
*/
|
|
293
|
+
export function passkeyStorage(
|
|
294
|
+
options: PasskeyStorageOptions = {},
|
|
295
|
+
): KeyringStorage {
|
|
296
|
+
const rpName = options.rpName ?? 'BYOK'
|
|
297
|
+
const userName = options.userName ?? 'byok-keyring'
|
|
298
|
+
const { rpId } = options
|
|
299
|
+
const dbName = options.dbName ?? DEFAULT_DB
|
|
300
|
+
|
|
301
|
+
let cachedKey: CryptoKey | null = null
|
|
302
|
+
let cachedMeta: {
|
|
303
|
+
credentialId: ArrayBuffer
|
|
304
|
+
salt: Uint8Array<ArrayBuffer>
|
|
305
|
+
} | null = null
|
|
306
|
+
|
|
307
|
+
// Obtain the AES key, running exactly one WebAuthn ceremony if it isn't
|
|
308
|
+
// already cached for this session (unlock if a passkey exists, else register).
|
|
309
|
+
async function ensureKey(): Promise<{
|
|
310
|
+
key: CryptoKey
|
|
311
|
+
credentialId: ArrayBuffer
|
|
312
|
+
salt: Uint8Array<ArrayBuffer>
|
|
313
|
+
}> {
|
|
314
|
+
if (cachedKey && cachedMeta) {
|
|
315
|
+
return { key: cachedKey, ...cachedMeta }
|
|
316
|
+
}
|
|
317
|
+
const existing = await idbGet(dbName)
|
|
318
|
+
if (existing) {
|
|
319
|
+
const prf = await evaluatePrf(existing.credentialId, existing.salt)
|
|
320
|
+
cachedKey = await deriveAesKey(prf)
|
|
321
|
+
cachedMeta = {
|
|
322
|
+
credentialId: existing.credentialId,
|
|
323
|
+
salt: new Uint8Array(existing.salt),
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
const reg = await registerPasskey(rpName, userName, rpId)
|
|
327
|
+
const prf = reg.prf ?? (await evaluatePrf(reg.credentialId, reg.salt))
|
|
328
|
+
cachedKey = await deriveAesKey(prf)
|
|
329
|
+
cachedMeta = { credentialId: reg.credentialId, salt: reg.salt }
|
|
330
|
+
}
|
|
331
|
+
return { key: cachedKey, ...cachedMeta }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
id: 'passkey',
|
|
336
|
+
label: 'Passkey-encrypted (this device)',
|
|
337
|
+
persistent: true,
|
|
338
|
+
unlockable: true,
|
|
339
|
+
warning:
|
|
340
|
+
'Keys are encrypted with your passkey and unlocked with biometrics. ' +
|
|
341
|
+
'This protects saved keys if your device is stolen, but not against code ' +
|
|
342
|
+
'running on this page after you unlock.',
|
|
343
|
+
peek: async () => {
|
|
344
|
+
// Unencrypted last-4 sidecar — no key material, no unlock ceremony.
|
|
345
|
+
const existing = await idbGet(dbName)
|
|
346
|
+
return existing?.preview ?? {}
|
|
347
|
+
},
|
|
348
|
+
load: async () => {
|
|
349
|
+
const existing = await idbGet(dbName)
|
|
350
|
+
if (!existing) return {}
|
|
351
|
+
const { key } = await ensureKey()
|
|
352
|
+
return decryptKeyring(key, existing.iv, existing.ciphertext)
|
|
353
|
+
},
|
|
354
|
+
save: async (keys) => {
|
|
355
|
+
const existing = await idbGet(dbName)
|
|
356
|
+
const hasKeys = Object.values(keys).some(Boolean)
|
|
357
|
+
// First save with an empty keyring is a no-op — avoids a passkey ceremony
|
|
358
|
+
// when another storage tier writes an empty ring.
|
|
359
|
+
if (!hasKeys && !existing) return
|
|
360
|
+
|
|
361
|
+
const { key, credentialId, salt } = await ensureKey()
|
|
362
|
+
const { iv, ciphertext } = await encryptKeyring(key, keys)
|
|
363
|
+
await idbPut(dbName, {
|
|
364
|
+
id: RECORD_ID,
|
|
365
|
+
credentialId,
|
|
366
|
+
salt: salt.buffer,
|
|
367
|
+
iv,
|
|
368
|
+
ciphertext,
|
|
369
|
+
preview: previewOf(keys),
|
|
370
|
+
})
|
|
371
|
+
},
|
|
372
|
+
clear: async () => {
|
|
373
|
+
cachedKey = null
|
|
374
|
+
cachedMeta = null
|
|
375
|
+
await idbClear(dbName)
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Passkey-encrypted storage when WebAuthn is available in a secure context.
|
|
382
|
+
* Otherwise session memory, with a warning — this is not an automatic PRF
|
|
383
|
+
* fallback. First save still throws if the authenticator lacks PRF.
|
|
384
|
+
*/
|
|
385
|
+
export function defaultByokStorage(
|
|
386
|
+
options?: PasskeyStorageOptions,
|
|
387
|
+
): KeyringStorage {
|
|
388
|
+
const secure =
|
|
389
|
+
typeof globalThis.isSecureContext !== 'boolean' ||
|
|
390
|
+
globalThis.isSecureContext
|
|
391
|
+
if (!isPasskeyStorageSupported() || !secure) {
|
|
392
|
+
return {
|
|
393
|
+
...memoryStorage(),
|
|
394
|
+
warning:
|
|
395
|
+
'Passkeys are unavailable in this context. Keys stay in memory for this tab only.',
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return passkeyStorage(options)
|
|
399
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ByokUnresolvedProviderError, isProviderId } from '@tanstack/ai/byok'
|
|
2
|
+
import type { ProviderId } from '@tanstack/ai/byok'
|
|
3
|
+
import type { ByokClient } from './client'
|
|
4
|
+
|
|
5
|
+
export function resolveByokProviderId(
|
|
6
|
+
byokProvider: (() => string | undefined) | undefined,
|
|
7
|
+
...candidates: Array<unknown>
|
|
8
|
+
): ProviderId | undefined {
|
|
9
|
+
const fromFn = byokProvider?.()
|
|
10
|
+
if (isProviderId(fromFn)) return fromFn
|
|
11
|
+
for (const candidate of candidates) {
|
|
12
|
+
if (isProviderId(candidate)) return candidate
|
|
13
|
+
}
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Prepare and stamp headers for one resolved slug. Throws instead of
|
|
19
|
+
* attaching every stored key when the slug is missing.
|
|
20
|
+
*/
|
|
21
|
+
export async function prepareResolvedByokHeaders(
|
|
22
|
+
byok: ByokClient,
|
|
23
|
+
provider: ProviderId | undefined,
|
|
24
|
+
): Promise<Record<string, string>> {
|
|
25
|
+
if (!provider) {
|
|
26
|
+
throw new ByokUnresolvedProviderError()
|
|
27
|
+
}
|
|
28
|
+
await byok.prepare(provider)
|
|
29
|
+
return byok.headers(provider)
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ProviderId } from '@tanstack/ai/byok'
|
|
2
|
+
|
|
3
|
+
export type Keyring = Partial<Record<ProviderId, string>>
|
|
4
|
+
export type KeyPreview = Partial<Record<ProviderId, string>>
|
|
5
|
+
|
|
6
|
+
export interface KeyringStorage {
|
|
7
|
+
readonly id: string
|
|
8
|
+
readonly label: string
|
|
9
|
+
readonly persistent: boolean
|
|
10
|
+
readonly unlockable?: boolean
|
|
11
|
+
readonly warning?: string
|
|
12
|
+
peek?: () => KeyPreview | Promise<KeyPreview>
|
|
13
|
+
load: () => Keyring | Promise<Keyring>
|
|
14
|
+
save: (keys: Keyring) => void | Promise<void>
|
|
15
|
+
clear: () => void | Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function memoryStorage(): KeyringStorage {
|
|
19
|
+
return {
|
|
20
|
+
id: 'memory',
|
|
21
|
+
label: 'Session only (not saved)',
|
|
22
|
+
persistent: false,
|
|
23
|
+
warning:
|
|
24
|
+
'Keys stay in memory for this tab only. They are not saved across reloads.',
|
|
25
|
+
load: () => ({}),
|
|
26
|
+
save: () => {},
|
|
27
|
+
clear: () => {},
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/byok.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { defineByok, ByokClient, EMPTY_BYOK_SNAPSHOT } from './byok/client'
|
|
2
|
+
export type {
|
|
3
|
+
ByokPrompt,
|
|
4
|
+
ByokSnapshot,
|
|
5
|
+
DefineByokOptions,
|
|
6
|
+
KeyStatus,
|
|
7
|
+
} from './byok/client'
|
|
8
|
+
export { defineByokProvider } from '@tanstack/ai/byok'
|
|
9
|
+
export type { ByokProvider, ByokProviderInit } from '@tanstack/ai/byok'
|
|
10
|
+
export { memoryStorage } from './byok/storage'
|
|
11
|
+
export type { KeyPreview, Keyring, KeyringStorage } from './byok/storage'
|
|
12
|
+
export {
|
|
13
|
+
defaultByokStorage,
|
|
14
|
+
decryptKeyring,
|
|
15
|
+
deriveAesKey,
|
|
16
|
+
encryptKeyring,
|
|
17
|
+
isPasskeyStorageSupported,
|
|
18
|
+
passkeyStorage,
|
|
19
|
+
} from './byok/passkey'
|
|
20
|
+
export type { PasskeyStorageOptions } from './byok/passkey'
|