@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.
Files changed (49) 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 +16 -3
  16. package/dist/esm/chat-client.js +49 -31
  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 +33 -9
  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/sse-parser.js +8 -1
  30. package/dist/esm/sse-parser.js.map +1 -1
  31. package/dist/esm/types.d.ts +29 -3
  32. package/dist/esm/types.js.map +1 -1
  33. package/dist/esm/video-generation-client.d.ts +3 -1
  34. package/dist/esm/video-generation-client.js +25 -6
  35. package/dist/esm/video-generation-client.js.map +1 -1
  36. package/package.json +6 -2
  37. package/src/byok/client.ts +360 -0
  38. package/src/byok/passkey.ts +399 -0
  39. package/src/byok/resolve.ts +30 -0
  40. package/src/byok/storage.ts +29 -0
  41. package/src/byok.ts +20 -0
  42. package/src/chat-client.ts +95 -65
  43. package/src/connection-adapters.ts +41 -12
  44. package/src/generation-client.ts +48 -4
  45. package/src/generation-types.ts +16 -0
  46. package/src/interrupt-manager.ts +20 -6
  47. package/src/sse-parser.ts +10 -0
  48. package/src/types.ts +31 -3
  49. package/src/video-generation-client.ts +45 -4
@@ -0,0 +1,52 @@
1
+ import { ProviderId } from '@tanstack/ai/byok';
2
+ import { Keyring, KeyringStorage } from './storage.js';
3
+ export type KeyStatus = {
4
+ state: 'empty';
5
+ } | {
6
+ state: 'set';
7
+ masked: string;
8
+ } | {
9
+ state: 'locked';
10
+ masked: string;
11
+ } | {
12
+ state: 'error';
13
+ masked: string;
14
+ message: string;
15
+ };
16
+ export type ByokPrompt = {
17
+ provider: ProviderId;
18
+ reason: 'missing' | 'locked';
19
+ };
20
+ export type ByokSnapshot = {
21
+ status: Partial<Record<string, KeyStatus>>;
22
+ locked: boolean;
23
+ prompt: ByokPrompt | null;
24
+ storageError: string | null;
25
+ };
26
+ export interface DefineByokOptions {
27
+ storage?: KeyringStorage;
28
+ }
29
+ export declare const EMPTY_BYOK_SNAPSHOT: ByokSnapshot;
30
+ export declare class ByokClient {
31
+ #private;
32
+ readonly storage: KeyringStorage;
33
+ constructor(options?: DefineByokOptions);
34
+ /** Resolves when constructor hydration (peek/load) finishes. */
35
+ ready: () => Promise<void>;
36
+ subscribe: (listener: () => void) => (() => void);
37
+ getSnapshot: () => ByokSnapshot;
38
+ keys(): Keyring;
39
+ request(provider: ProviderId, reason: ByokPrompt['reason']): void;
40
+ /**
41
+ * `true` — the server can fill any slug from env.
42
+ * `false` — no env coverage.
43
+ * A record merges per-id flags and clears the all/none boolean.
44
+ */
45
+ setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void;
46
+ headers(provider?: ProviderId): Record<string, string>;
47
+ prepare(provider?: ProviderId): Promise<void>;
48
+ update(providerOrKey: ProviderId | string, key?: string): Promise<void>;
49
+ clear(provider?: ProviderId): Promise<void>;
50
+ unlock(): Promise<void>;
51
+ }
52
+ export declare function defineByok(options?: DefineByokOptions): ByokClient;
@@ -0,0 +1,300 @@
1
+ import { memoryStorage } from "./storage.js";
2
+ import { ByokBlockedError, byokHeaderName, isProviderId, maskKey } from "@tanstack/ai/byok";
3
+ //#region src/byok/client.ts
4
+ var EMPTY_BYOK_SNAPSHOT = {
5
+ status: {},
6
+ locked: false,
7
+ prompt: null,
8
+ storageError: null
9
+ };
10
+ function requireProviderId(value) {
11
+ if (!isProviderId(value)) throw new Error(`Invalid BYOK provider id: ${value}`);
12
+ return value;
13
+ }
14
+ function errorMessage(error, fallback) {
15
+ return error instanceof Error && error.message.length > 0 ? error.message : fallback;
16
+ }
17
+ function isByokCeremonyCancel(error) {
18
+ if (typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "NotAllowedError") return true;
19
+ if (error instanceof Error) {
20
+ if (error.name === "NotAllowedError") return true;
21
+ const message = error.message.toLowerCase();
22
+ return message.includes("cancelled") || message.includes("not allowed");
23
+ }
24
+ return false;
25
+ }
26
+ function sanitizeKeyring(value) {
27
+ if (typeof value !== "object" || value === null) return {};
28
+ const keys = {};
29
+ for (const [provider, key] of Object.entries(value)) if (isProviderId(provider) && typeof key === "string" && key.length > 0) keys[provider] = key;
30
+ return keys;
31
+ }
32
+ var ByokClient = class {
33
+ storage;
34
+ #keys = {};
35
+ #statuses = {};
36
+ #locked;
37
+ #prompt = null;
38
+ #coverageAll = false;
39
+ #coverage = {};
40
+ #listeners = /* @__PURE__ */ new Set();
41
+ #snapshot;
42
+ #storageError = null;
43
+ #ready;
44
+ constructor(options = {}) {
45
+ this.storage = options.storage ?? memoryStorage();
46
+ this.#locked = Boolean(this.storage.unlockable);
47
+ this.#snapshot = this.#buildSnapshot();
48
+ this.#ready = this.#hydrate();
49
+ }
50
+ /** Resolves when constructor hydration (peek/load) finishes. */
51
+ ready = () => this.#ready;
52
+ subscribe = (listener) => {
53
+ this.#listeners.add(listener);
54
+ return () => {
55
+ this.#listeners.delete(listener);
56
+ };
57
+ };
58
+ getSnapshot = () => this.#snapshot;
59
+ #buildSnapshot() {
60
+ return {
61
+ status: { ...this.#statuses },
62
+ locked: this.#locked,
63
+ prompt: this.#prompt,
64
+ storageError: this.#storageError
65
+ };
66
+ }
67
+ keys() {
68
+ return { ...this.#keys };
69
+ }
70
+ request(provider, reason) {
71
+ this.#prompt = {
72
+ provider: requireProviderId(provider),
73
+ reason
74
+ };
75
+ this.#emit();
76
+ }
77
+ /**
78
+ * `true` — the server can fill any slug from env.
79
+ * `false` — no env coverage.
80
+ * A record merges per-id flags and clears the all/none boolean.
81
+ */
82
+ setServerCoverage(flags) {
83
+ if (typeof flags === "boolean") {
84
+ this.#coverageAll = flags;
85
+ this.#coverage = {};
86
+ return;
87
+ }
88
+ this.#coverageAll = false;
89
+ this.#coverage = {
90
+ ...this.#coverage,
91
+ ...flags
92
+ };
93
+ }
94
+ #hasCoverage(provider) {
95
+ return this.#coverageAll || this.#coverage[provider] === true;
96
+ }
97
+ headers(provider) {
98
+ const headers = {};
99
+ if (provider) {
100
+ requireProviderId(provider);
101
+ const key = this.#keys[provider];
102
+ if (key) headers[byokHeaderName(provider)] = key;
103
+ return headers;
104
+ }
105
+ for (const [id, key] of Object.entries(this.#keys)) if (key) headers[byokHeaderName(id)] = key;
106
+ return headers;
107
+ }
108
+ async prepare(provider) {
109
+ await this.#ready;
110
+ if (this.storage.unlockable && this.#locked) await this.unlock();
111
+ if (!provider) return;
112
+ requireProviderId(provider);
113
+ if (this.#keys[provider]) return;
114
+ if (this.#hasCoverage(provider)) return;
115
+ this.request(provider, "missing");
116
+ throw new ByokBlockedError(provider, "missing");
117
+ }
118
+ async update(providerOrKey, key) {
119
+ await this.#ready;
120
+ let provider;
121
+ let nextKey;
122
+ if (key === void 0) {
123
+ if (!this.#prompt) throw new Error("byok.update(key) cannot run when prompt is null");
124
+ provider = this.#prompt.provider;
125
+ nextKey = providerOrKey;
126
+ } else {
127
+ provider = requireProviderId(providerOrKey);
128
+ nextKey = key;
129
+ }
130
+ nextKey = nextKey.trim();
131
+ if (nextKey.length === 0) throw new Error("BYOK key must be non-empty");
132
+ if (this.storage.unlockable && this.#locked) await this.unlock();
133
+ const previousKeys = this.#keys;
134
+ const previousStatuses = { ...this.#statuses };
135
+ const previousLocked = this.#locked;
136
+ const previousPrompt = this.#prompt;
137
+ const previousStorageError = this.#storageError;
138
+ const next = {
139
+ ...this.#keys,
140
+ [provider]: nextKey
141
+ };
142
+ try {
143
+ await this.storage.save(next);
144
+ } catch (error) {
145
+ this.#keys = previousKeys;
146
+ this.#statuses = previousStatuses;
147
+ this.#locked = previousLocked;
148
+ this.#prompt = previousPrompt;
149
+ this.#storageError = previousStorageError;
150
+ this.#statuses[provider] = {
151
+ state: "error",
152
+ masked: maskKey(nextKey),
153
+ message: errorMessage(error, `Failed to persist ${provider} key`)
154
+ };
155
+ this.#emit();
156
+ throw error;
157
+ }
158
+ this.#keys = next;
159
+ this.#statuses[provider] = {
160
+ state: "set",
161
+ masked: maskKey(nextKey)
162
+ };
163
+ this.#locked = false;
164
+ this.#prompt = null;
165
+ this.#storageError = null;
166
+ this.#emit();
167
+ }
168
+ async clear(provider) {
169
+ await this.#ready;
170
+ if (provider) {
171
+ requireProviderId(provider);
172
+ if (this.storage.unlockable && this.#locked) await this.unlock();
173
+ const previousKeys = this.#keys;
174
+ const previousStatuses = { ...this.#statuses };
175
+ const next = { ...this.#keys };
176
+ delete next[provider];
177
+ try {
178
+ await this.storage.save(next);
179
+ } catch (error) {
180
+ this.#keys = previousKeys;
181
+ this.#statuses = previousStatuses;
182
+ this.#emit();
183
+ throw error;
184
+ }
185
+ this.#keys = next;
186
+ delete this.#statuses[provider];
187
+ this.#emit();
188
+ return;
189
+ }
190
+ const previousKeys = this.#keys;
191
+ const previousStatuses = this.#statuses;
192
+ const previousLocked = this.#locked;
193
+ const previousPrompt = this.#prompt;
194
+ try {
195
+ await this.storage.clear();
196
+ } catch (error) {
197
+ this.#keys = previousKeys;
198
+ this.#statuses = previousStatuses;
199
+ this.#locked = previousLocked;
200
+ this.#prompt = previousPrompt;
201
+ this.#emit();
202
+ throw error;
203
+ }
204
+ this.#keys = {};
205
+ this.#statuses = {};
206
+ this.#locked = false;
207
+ this.#prompt = null;
208
+ this.#storageError = null;
209
+ this.#emit();
210
+ }
211
+ async unlock() {
212
+ await this.#ready;
213
+ if (!this.storage.unlockable) return;
214
+ try {
215
+ const loaded = sanitizeKeyring(await this.storage.load());
216
+ this.#keys = {
217
+ ...loaded,
218
+ ...this.#keys
219
+ };
220
+ for (const [id, value] of Object.entries(loaded)) {
221
+ if (!isProviderId(id) || !value) continue;
222
+ const existing = this.#statuses[id];
223
+ if (!existing || existing.state === "locked") this.#statuses[id] = {
224
+ state: "set",
225
+ masked: maskKey(value)
226
+ };
227
+ }
228
+ this.#locked = false;
229
+ this.#storageError = null;
230
+ this.#emit();
231
+ } catch (error) {
232
+ if (isByokCeremonyCancel(error)) {
233
+ const provider = this.#lockedProvider();
234
+ if (provider) {
235
+ this.request(provider, "locked");
236
+ throw new ByokBlockedError(provider, "locked");
237
+ }
238
+ }
239
+ this.#storageError = errorMessage(error, "Failed to unlock keyring");
240
+ this.#emit();
241
+ throw error instanceof Error ? error : new Error(String(error));
242
+ }
243
+ }
244
+ #lockedProvider() {
245
+ if (this.#prompt && isProviderId(this.#prompt.provider)) return this.#prompt.provider;
246
+ for (const [id, status] of Object.entries(this.#statuses)) if (status?.state === "locked" && isProviderId(id)) return id;
247
+ }
248
+ async #hydrate() {
249
+ if (this.storage.unlockable) {
250
+ if (!this.storage.peek) return;
251
+ try {
252
+ const preview = await this.storage.peek();
253
+ for (const [id, last4] of Object.entries(preview)) {
254
+ if (!isProviderId(id) || this.#statuses[id]) continue;
255
+ this.#statuses[id] = {
256
+ state: "locked",
257
+ masked: last4 ? last4 : "••"
258
+ };
259
+ }
260
+ this.#locked = Object.keys(preview).length > 0;
261
+ this.#storageError = null;
262
+ this.#emit();
263
+ } catch (error) {
264
+ this.#storageError = errorMessage(error, "Failed to read keyring");
265
+ this.#emit();
266
+ }
267
+ return;
268
+ }
269
+ try {
270
+ const loaded = sanitizeKeyring(await this.storage.load());
271
+ this.#keys = {
272
+ ...loaded,
273
+ ...this.#keys
274
+ };
275
+ for (const [id, value] of Object.entries(loaded)) {
276
+ if (!isProviderId(id) || !value) continue;
277
+ this.#statuses[id] = {
278
+ state: "set",
279
+ masked: maskKey(value)
280
+ };
281
+ }
282
+ this.#storageError = null;
283
+ this.#emit();
284
+ } catch (error) {
285
+ this.#storageError = errorMessage(error, "Failed to read keyring");
286
+ this.#emit();
287
+ }
288
+ }
289
+ #emit() {
290
+ this.#snapshot = this.#buildSnapshot();
291
+ for (const listener of this.#listeners) listener();
292
+ }
293
+ };
294
+ function defineByok(options = {}) {
295
+ return new ByokClient(options);
296
+ }
297
+ //#endregion
298
+ export { ByokClient, EMPTY_BYOK_SNAPSHOT, defineByok };
299
+
300
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["#listeners","#ready","#locked","#snapshot","#buildSnapshot","#hydrate","#statuses","#prompt","#storageError","#keys","#emit","#coverageAll","#coverage","#hasCoverage","#lockedProvider"],"sources":["../../../src/byok/client.ts"],"sourcesContent":["import {\n ByokBlockedError,\n byokHeaderName,\n isProviderId,\n maskKey,\n} from '@tanstack/ai/byok'\nimport { memoryStorage } from './storage'\nimport type { ProviderId } from '@tanstack/ai/byok'\nimport type { Keyring, KeyringStorage } from './storage'\n\nexport type KeyStatus =\n | { state: 'empty' }\n | { state: 'set'; masked: string }\n | { state: 'locked'; masked: string }\n | { state: 'error'; masked: string; message: string }\n\nexport type ByokPrompt = {\n provider: ProviderId\n reason: 'missing' | 'locked'\n}\n\nexport type ByokSnapshot = {\n status: Partial<Record<string, KeyStatus>>\n locked: boolean\n prompt: ByokPrompt | null\n storageError: string | null\n}\n\nexport interface DefineByokOptions {\n storage?: KeyringStorage\n}\n\nexport const EMPTY_BYOK_SNAPSHOT: ByokSnapshot = {\n status: {},\n locked: false,\n prompt: null,\n storageError: null,\n}\n\nfunction requireProviderId(value: string): ProviderId {\n if (!isProviderId(value)) {\n throw new Error(`Invalid BYOK provider id: ${value}`)\n }\n return value\n}\n\nfunction errorMessage(error: unknown, fallback: string): string {\n return error instanceof Error && error.message.length > 0\n ? error.message\n : fallback\n}\n\nfunction isByokCeremonyCancel(error: unknown): boolean {\n if (\n typeof DOMException !== 'undefined' &&\n error instanceof DOMException &&\n error.name === 'NotAllowedError'\n ) {\n return true\n }\n if (error instanceof Error) {\n if (error.name === 'NotAllowedError') return true\n const message = error.message.toLowerCase()\n return message.includes('cancelled') || message.includes('not allowed')\n }\n return false\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\nexport class ByokClient {\n readonly storage: KeyringStorage\n #keys: Keyring = {}\n #statuses: Partial<Record<string, KeyStatus>> = {}\n #locked: boolean\n #prompt: ByokPrompt | null = null\n #coverageAll = false\n #coverage: Record<string, boolean> = {}\n readonly #listeners = new Set<() => void>()\n #snapshot: ByokSnapshot\n #storageError: string | null = null\n readonly #ready: Promise<void>\n\n constructor(options: DefineByokOptions = {}) {\n this.storage = options.storage ?? memoryStorage()\n this.#locked = Boolean(this.storage.unlockable)\n this.#snapshot = this.#buildSnapshot()\n this.#ready = this.#hydrate()\n }\n\n /** Resolves when constructor hydration (peek/load) finishes. */\n ready = (): Promise<void> => this.#ready\n\n subscribe = (listener: () => void): (() => void) => {\n this.#listeners.add(listener)\n return () => {\n this.#listeners.delete(listener)\n }\n }\n\n getSnapshot = (): ByokSnapshot => this.#snapshot\n\n #buildSnapshot(): ByokSnapshot {\n return {\n status: { ...this.#statuses },\n locked: this.#locked,\n prompt: this.#prompt,\n storageError: this.#storageError,\n }\n }\n\n keys(): Keyring {\n return { ...this.#keys }\n }\n\n request(provider: ProviderId, reason: ByokPrompt['reason']): void {\n this.#prompt = { provider: requireProviderId(provider), reason }\n this.#emit()\n }\n\n /**\n * `true` — the server can fill any slug from env.\n * `false` — no env coverage.\n * A record merges per-id flags and clears the all/none boolean.\n */\n setServerCoverage(flags: boolean | Readonly<Record<string, boolean>>): void {\n if (typeof flags === 'boolean') {\n this.#coverageAll = flags\n this.#coverage = {}\n return\n }\n this.#coverageAll = false\n this.#coverage = { ...this.#coverage, ...flags }\n }\n\n #hasCoverage(provider: string): boolean {\n return this.#coverageAll || this.#coverage[provider] === true\n }\n\n headers(provider?: ProviderId): Record<string, string> {\n const headers: Record<string, string> = {}\n if (provider) {\n requireProviderId(provider)\n const key = this.#keys[provider]\n if (key) headers[byokHeaderName(provider)] = key\n return headers\n }\n for (const [id, key] of Object.entries(this.#keys)) {\n if (key) headers[byokHeaderName(id)] = key\n }\n return headers\n }\n\n async prepare(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n if (!provider) return\n requireProviderId(provider)\n if (this.#keys[provider]) return\n if (this.#hasCoverage(provider)) return\n this.request(provider, 'missing')\n throw new ByokBlockedError(provider, 'missing')\n }\n\n async update(\n providerOrKey: ProviderId | string,\n key?: string,\n ): Promise<void> {\n await this.#ready\n let provider: ProviderId\n let nextKey: string\n if (key === undefined) {\n if (!this.#prompt) {\n throw new Error('byok.update(key) cannot run when prompt is null')\n }\n provider = this.#prompt.provider\n nextKey = providerOrKey\n } else {\n provider = requireProviderId(providerOrKey)\n nextKey = key\n }\n nextKey = nextKey.trim()\n if (nextKey.length === 0) {\n throw new Error('BYOK key must be non-empty')\n }\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n const previousStorageError = this.#storageError\n const next = { ...this.#keys, [provider]: nextKey }\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#storageError = previousStorageError\n this.#statuses[provider] = {\n state: 'error',\n masked: maskKey(nextKey),\n message: errorMessage(error, `Failed to persist ${provider} key`),\n }\n this.#emit()\n throw error\n }\n this.#keys = next\n this.#statuses[provider] = { state: 'set', masked: maskKey(nextKey) }\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async clear(provider?: ProviderId): Promise<void> {\n await this.#ready\n if (provider) {\n requireProviderId(provider)\n if (this.storage.unlockable && this.#locked) {\n await this.unlock()\n }\n const previousKeys = this.#keys\n const previousStatuses = { ...this.#statuses }\n const next = { ...this.#keys }\n delete next[provider]\n try {\n await this.storage.save(next)\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#emit()\n throw error\n }\n this.#keys = next\n delete this.#statuses[provider]\n this.#emit()\n return\n }\n const previousKeys = this.#keys\n const previousStatuses = this.#statuses\n const previousLocked = this.#locked\n const previousPrompt = this.#prompt\n try {\n await this.storage.clear()\n } catch (error) {\n this.#keys = previousKeys\n this.#statuses = previousStatuses\n this.#locked = previousLocked\n this.#prompt = previousPrompt\n this.#emit()\n throw error\n }\n this.#keys = {}\n this.#statuses = {}\n this.#locked = false\n this.#prompt = null\n this.#storageError = null\n this.#emit()\n }\n\n async unlock(): Promise<void> {\n await this.#ready\n if (!this.storage.unlockable) return\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n const existing = this.#statuses[id]\n if (!existing || existing.state === 'locked') {\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n }\n this.#locked = false\n this.#storageError = null\n this.#emit()\n } catch (error) {\n if (isByokCeremonyCancel(error)) {\n const provider = this.#lockedProvider()\n if (provider) {\n this.request(provider, 'locked')\n throw new ByokBlockedError(provider, 'locked')\n }\n }\n this.#storageError = errorMessage(error, 'Failed to unlock keyring')\n this.#emit()\n throw error instanceof Error ? error : new Error(String(error))\n }\n }\n\n #lockedProvider(): ProviderId | undefined {\n if (this.#prompt && isProviderId(this.#prompt.provider)) {\n return this.#prompt.provider\n }\n for (const [id, status] of Object.entries(this.#statuses)) {\n if (status?.state === 'locked' && isProviderId(id)) return id\n }\n return undefined\n }\n\n async #hydrate(): Promise<void> {\n if (this.storage.unlockable) {\n if (!this.storage.peek) return\n try {\n const preview = await this.storage.peek()\n for (const [id, last4] of Object.entries(preview)) {\n if (!isProviderId(id) || this.#statuses[id]) continue\n this.#statuses[id] = {\n state: 'locked',\n masked: last4 ? last4 : '••',\n }\n }\n this.#locked = Object.keys(preview).length > 0\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n return\n }\n try {\n const loaded = sanitizeKeyring(await this.storage.load())\n this.#keys = { ...loaded, ...this.#keys }\n for (const [id, value] of Object.entries(loaded)) {\n if (!isProviderId(id) || !value) continue\n this.#statuses[id] = { state: 'set', masked: maskKey(value) }\n }\n this.#storageError = null\n this.#emit()\n } catch (error) {\n this.#storageError = errorMessage(error, 'Failed to read keyring')\n this.#emit()\n }\n }\n\n #emit(): void {\n this.#snapshot = this.#buildSnapshot()\n for (const listener of this.#listeners) listener()\n }\n}\n\nexport function defineByok(options: DefineByokOptions = {}): ByokClient {\n return new ByokClient(options)\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { Keyring, KeyringStorage } from './storage.js';
2
+ /**
3
+ * Whether the current environment exposes WebAuthn. Actual PRF support can
4
+ * only be confirmed during registration; `passkeyStorage` throws if the
5
+ * chosen authenticator does not support PRF.
6
+ */
7
+ export declare function isPasskeyStorageSupported(): boolean;
8
+ /** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */
9
+ export declare function deriveAesKey(prfOutput: BufferSource): Promise<CryptoKey>;
10
+ export declare function encryptKeyring(key: CryptoKey, keys: Keyring): Promise<{
11
+ iv: ArrayBuffer;
12
+ ciphertext: ArrayBuffer;
13
+ }>;
14
+ export declare function decryptKeyring(key: CryptoKey, iv: BufferSource, ciphertext: BufferSource): Promise<Keyring>;
15
+ export interface PasskeyStorageOptions {
16
+ /** Relying-party name shown in the passkey prompt. */
17
+ rpName?: string;
18
+ /** Username label attached to the created passkey. */
19
+ userName?: string;
20
+ /**
21
+ * WebAuthn Relying Party ID. Omit to bind the passkey to the current origin's
22
+ * effective domain (the default — no central/hardcoded domain). Set it to a
23
+ * registrable parent domain to share the credential across subdomains of your
24
+ * own deployment. The encrypted keyring is never portable across unrelated
25
+ * domains.
26
+ */
27
+ rpId?: string;
28
+ /** IndexedDB database name. Defaults to `byok`. */
29
+ dbName?: string;
30
+ }
31
+ /**
32
+ * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so
33
+ * nothing is decrypted until the user calls `unlock()` (or saves a key, which
34
+ * registers a passkey on first use). The derived key is cached in memory for
35
+ * the session so repeated saves don't re-prompt.
36
+ */
37
+ export declare function passkeyStorage(options?: PasskeyStorageOptions): KeyringStorage;
38
+ /**
39
+ * Passkey-encrypted storage when WebAuthn is available in a secure context.
40
+ * Otherwise session memory, with a warning — this is not an automatic PRF
41
+ * fallback. First save still throws if the authenticator lacks PRF.
42
+ */
43
+ export declare function defaultByokStorage(options?: PasskeyStorageOptions): KeyringStorage;
@@ -0,0 +1,251 @@
1
+ import { memoryStorage } from "./storage.js";
2
+ import { isProviderId } from "@tanstack/ai/byok";
3
+ //#region src/byok/passkey.ts
4
+ /**
5
+ * Passkey-encrypted keyring storage (WebAuthn PRF → HKDF → AES-256-GCM).
6
+ *
7
+ * The keyring is encrypted at rest in IndexedDB with an AES-256-GCM key derived
8
+ * from a passkey's PRF output, unwrapped on demand with a biometric/PIN tap.
9
+ * Decryption happens entirely client-side with the user present — no server,
10
+ * no custodian.
11
+ *
12
+ * Honest scope: this protects against at-rest theft (stolen device,
13
+ * storage-dumping extension, backups). It does NOT defeat live in-page XSS —
14
+ * an attacker running JS in the origin after the user unlocks can read the
15
+ * decrypted keys from memory.
16
+ */
17
+ var STORE_NAME = "keyring";
18
+ var RECORD_ID = "default";
19
+ var HKDF_INFO = "byok:keyring:v1";
20
+ var DEFAULT_DB = "byok";
21
+ function sanitizeKeyring(value) {
22
+ if (typeof value !== "object" || value === null) return {};
23
+ const keys = {};
24
+ for (const [provider, key] of Object.entries(value)) if (isProviderId(provider) && typeof key === "string" && key.length > 0) keys[provider] = key;
25
+ return keys;
26
+ }
27
+ /** Build the non-sensitive `provider → last 4` preview from a keyring. */
28
+ function previewOf(keys) {
29
+ const preview = {};
30
+ for (const [provider, key] of Object.entries(keys)) {
31
+ if (!key || !isProviderId(provider)) continue;
32
+ preview[provider] = key.length > 4 ? key.slice(-4) : "";
33
+ }
34
+ return preview;
35
+ }
36
+ /**
37
+ * Whether the current environment exposes WebAuthn. Actual PRF support can
38
+ * only be confirmed during registration; `passkeyStorage` throws if the
39
+ * chosen authenticator does not support PRF.
40
+ */
41
+ function isPasskeyStorageSupported() {
42
+ return typeof globalThis !== "undefined" && typeof globalThis.PublicKeyCredential !== "undefined" && typeof globalThis.navigator !== "undefined" && typeof globalThis.navigator.credentials.create === "function";
43
+ }
44
+ /** Derive a non-extractable AES-256-GCM key from a 32-byte PRF output. */
45
+ async function deriveAesKey(prfOutput) {
46
+ const base = await crypto.subtle.importKey("raw", prfOutput, "HKDF", false, ["deriveKey"]);
47
+ return crypto.subtle.deriveKey({
48
+ name: "HKDF",
49
+ hash: "SHA-256",
50
+ salt: /* @__PURE__ */ new Uint8Array(0),
51
+ info: new TextEncoder().encode(HKDF_INFO)
52
+ }, base, {
53
+ name: "AES-GCM",
54
+ length: 256
55
+ }, false, ["encrypt", "decrypt"]);
56
+ }
57
+ async function encryptKeyring(key, keys) {
58
+ const iv = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(12));
59
+ const plaintext = new TextEncoder().encode(JSON.stringify(keys));
60
+ const ciphertext = await crypto.subtle.encrypt({
61
+ name: "AES-GCM",
62
+ iv
63
+ }, key, plaintext);
64
+ return {
65
+ iv: iv.buffer,
66
+ ciphertext
67
+ };
68
+ }
69
+ async function decryptKeyring(key, iv, ciphertext) {
70
+ const plaintext = await crypto.subtle.decrypt({
71
+ name: "AES-GCM",
72
+ iv
73
+ }, key, ciphertext);
74
+ return sanitizeKeyring(JSON.parse(new TextDecoder().decode(plaintext)));
75
+ }
76
+ function openDb(dbName) {
77
+ return new Promise((resolve, reject) => {
78
+ const request = indexedDB.open(dbName, 1);
79
+ request.onupgradeneeded = () => {
80
+ request.result.createObjectStore(STORE_NAME, { keyPath: "id" });
81
+ };
82
+ request.onsuccess = () => resolve(request.result);
83
+ request.onerror = () => reject(request.error);
84
+ });
85
+ }
86
+ function idbGet(dbName) {
87
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
88
+ const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(RECORD_ID);
89
+ request.onsuccess = () => resolve(request.result ?? null);
90
+ request.onerror = () => reject(request.error);
91
+ }));
92
+ }
93
+ function idbPut(dbName, record) {
94
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
95
+ const tx = db.transaction(STORE_NAME, "readwrite");
96
+ tx.objectStore(STORE_NAME).put(record);
97
+ tx.oncomplete = () => resolve();
98
+ tx.onerror = () => reject(tx.error);
99
+ }));
100
+ }
101
+ function idbClear(dbName) {
102
+ return openDb(dbName).then((db) => new Promise((resolve, reject) => {
103
+ const tx = db.transaction(STORE_NAME, "readwrite");
104
+ tx.objectStore(STORE_NAME).delete(RECORD_ID);
105
+ tx.oncomplete = () => resolve();
106
+ tx.onerror = () => reject(tx.error);
107
+ }));
108
+ }
109
+ function requirePublicKeyCredential(credential, action) {
110
+ if (!credential) throw new Error(`Passkey ${action} was cancelled`);
111
+ if (!(credential instanceof PublicKeyCredential)) throw new Error(`Unexpected credential type during ${action}`);
112
+ return credential;
113
+ }
114
+ async function registerPasskey(rpName, userName, rpId) {
115
+ const salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32));
116
+ const credential = requirePublicKeyCredential(await navigator.credentials.create({ publicKey: {
117
+ challenge: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)),
118
+ rp: rpId ? {
119
+ name: rpName,
120
+ id: rpId
121
+ } : { name: rpName },
122
+ user: {
123
+ id: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)),
124
+ name: userName,
125
+ displayName: userName
126
+ },
127
+ pubKeyCredParams: [{
128
+ type: "public-key",
129
+ alg: -7
130
+ }, {
131
+ type: "public-key",
132
+ alg: -257
133
+ }],
134
+ authenticatorSelection: {
135
+ residentKey: "required",
136
+ userVerification: "required"
137
+ },
138
+ extensions: { prf: { eval: { first: salt } } }
139
+ } }), "registration");
140
+ const prf = credential.getClientExtensionResults().prf;
141
+ if (!prf?.enabled) throw new Error("This authenticator does not support the WebAuthn PRF extension");
142
+ return {
143
+ credentialId: credential.rawId,
144
+ salt,
145
+ prf: prf.results?.first
146
+ };
147
+ }
148
+ async function evaluatePrf(credentialId, salt) {
149
+ const result = requirePublicKeyCredential(await navigator.credentials.get({ publicKey: {
150
+ challenge: crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)),
151
+ allowCredentials: [{
152
+ type: "public-key",
153
+ id: credentialId
154
+ }],
155
+ userVerification: "required",
156
+ extensions: { prf: { eval: { first: salt } } }
157
+ } }), "unlock").getClientExtensionResults().prf?.results?.first;
158
+ if (!result) throw new Error("Authenticator did not return a PRF result");
159
+ return result;
160
+ }
161
+ /**
162
+ * Passkey-encrypted persistence. `ByokClient` treats this as `unlockable`, so
163
+ * nothing is decrypted until the user calls `unlock()` (or saves a key, which
164
+ * registers a passkey on first use). The derived key is cached in memory for
165
+ * the session so repeated saves don't re-prompt.
166
+ */
167
+ function passkeyStorage(options = {}) {
168
+ const rpName = options.rpName ?? "BYOK";
169
+ const userName = options.userName ?? "byok-keyring";
170
+ const { rpId } = options;
171
+ const dbName = options.dbName ?? DEFAULT_DB;
172
+ let cachedKey = null;
173
+ let cachedMeta = null;
174
+ async function ensureKey() {
175
+ if (cachedKey && cachedMeta) return {
176
+ key: cachedKey,
177
+ ...cachedMeta
178
+ };
179
+ const existing = await idbGet(dbName);
180
+ if (existing) {
181
+ cachedKey = await deriveAesKey(await evaluatePrf(existing.credentialId, existing.salt));
182
+ cachedMeta = {
183
+ credentialId: existing.credentialId,
184
+ salt: new Uint8Array(existing.salt)
185
+ };
186
+ } else {
187
+ const reg = await registerPasskey(rpName, userName, rpId);
188
+ cachedKey = await deriveAesKey(reg.prf ?? await evaluatePrf(reg.credentialId, reg.salt));
189
+ cachedMeta = {
190
+ credentialId: reg.credentialId,
191
+ salt: reg.salt
192
+ };
193
+ }
194
+ return {
195
+ key: cachedKey,
196
+ ...cachedMeta
197
+ };
198
+ }
199
+ return {
200
+ id: "passkey",
201
+ label: "Passkey-encrypted (this device)",
202
+ persistent: true,
203
+ unlockable: true,
204
+ warning: "Keys are encrypted with your passkey and unlocked with biometrics. This protects saved keys if your device is stolen, but not against code running on this page after you unlock.",
205
+ peek: async () => {
206
+ return (await idbGet(dbName))?.preview ?? {};
207
+ },
208
+ load: async () => {
209
+ const existing = await idbGet(dbName);
210
+ if (!existing) return {};
211
+ const { key } = await ensureKey();
212
+ return decryptKeyring(key, existing.iv, existing.ciphertext);
213
+ },
214
+ save: async (keys) => {
215
+ const existing = await idbGet(dbName);
216
+ if (!Object.values(keys).some(Boolean) && !existing) return;
217
+ const { key, credentialId, salt } = await ensureKey();
218
+ const { iv, ciphertext } = await encryptKeyring(key, keys);
219
+ await idbPut(dbName, {
220
+ id: RECORD_ID,
221
+ credentialId,
222
+ salt: salt.buffer,
223
+ iv,
224
+ ciphertext,
225
+ preview: previewOf(keys)
226
+ });
227
+ },
228
+ clear: async () => {
229
+ cachedKey = null;
230
+ cachedMeta = null;
231
+ await idbClear(dbName);
232
+ }
233
+ };
234
+ }
235
+ /**
236
+ * Passkey-encrypted storage when WebAuthn is available in a secure context.
237
+ * Otherwise session memory, with a warning — this is not an automatic PRF
238
+ * fallback. First save still throws if the authenticator lacks PRF.
239
+ */
240
+ function defaultByokStorage(options) {
241
+ const secure = typeof globalThis.isSecureContext !== "boolean" || globalThis.isSecureContext;
242
+ if (!isPasskeyStorageSupported() || !secure) return {
243
+ ...memoryStorage(),
244
+ warning: "Passkeys are unavailable in this context. Keys stay in memory for this tab only."
245
+ };
246
+ return passkeyStorage(options);
247
+ }
248
+ //#endregion
249
+ export { decryptKeyring, defaultByokStorage, deriveAesKey, encryptKeyring, isPasskeyStorageSupported, passkeyStorage };
250
+
251
+ //# sourceMappingURL=passkey.js.map