@thru/passkey 0.2.12 → 0.2.14

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 (71) hide show
  1. package/README.md +73 -90
  2. package/dist/auth.cjs +672 -0
  3. package/dist/auth.cjs.map +1 -0
  4. package/dist/auth.d.cts +60 -0
  5. package/dist/auth.d.ts +60 -0
  6. package/dist/auth.js +422 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/chunk-2JHC7OOH.js +250 -0
  9. package/dist/chunk-2JHC7OOH.js.map +1 -0
  10. package/dist/chunk-75G2FPYW.js +54 -0
  11. package/dist/chunk-75G2FPYW.js.map +1 -0
  12. package/dist/chunk-B5SN7AS7.js +586 -0
  13. package/dist/chunk-B5SN7AS7.js.map +1 -0
  14. package/dist/chunk-LNDWK3FA.js +163 -0
  15. package/dist/chunk-LNDWK3FA.js.map +1 -0
  16. package/dist/index.cjs +27 -94
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +4 -187
  19. package/dist/index.d.ts +4 -187
  20. package/dist/index.js +47 -810
  21. package/dist/index.js.map +1 -1
  22. package/dist/mobile.cjs +301 -0
  23. package/dist/mobile.cjs.map +1 -0
  24. package/dist/mobile.d.cts +49 -0
  25. package/dist/mobile.d.ts +49 -0
  26. package/dist/mobile.js +41 -0
  27. package/dist/mobile.js.map +1 -0
  28. package/dist/popup.cjs +247 -0
  29. package/dist/popup.cjs.map +1 -0
  30. package/dist/popup.d.cts +22 -0
  31. package/dist/popup.d.ts +22 -0
  32. package/dist/popup.js +31 -0
  33. package/dist/popup.js.map +1 -0
  34. package/dist/server.cjs +351 -0
  35. package/dist/server.cjs.map +1 -0
  36. package/dist/server.d.cts +119 -0
  37. package/dist/server.d.ts +119 -0
  38. package/dist/server.js +340 -0
  39. package/dist/server.js.map +1 -0
  40. package/dist/types-_HRzmn-j.d.cts +125 -0
  41. package/dist/types-_HRzmn-j.d.ts +125 -0
  42. package/dist/web.cjs +758 -0
  43. package/dist/web.cjs.map +1 -0
  44. package/dist/web.d.cts +32 -0
  45. package/dist/web.d.ts +32 -0
  46. package/dist/web.js +60 -0
  47. package/dist/web.js.map +1 -0
  48. package/package.json +47 -2
  49. package/src/auth/execute-tx.ts +87 -0
  50. package/src/auth/index.ts +18 -0
  51. package/src/auth/types.ts +56 -0
  52. package/src/auth/use-passkey-auth.ts +428 -0
  53. package/src/index.ts +37 -39
  54. package/src/mobile/errors.ts +31 -0
  55. package/src/mobile/index.ts +33 -0
  56. package/src/mobile/passkey.ts +154 -0
  57. package/src/mobile/storage.ts +115 -0
  58. package/src/mobile/types.ts +24 -0
  59. package/src/popup-entry.ts +33 -0
  60. package/src/popup-service.ts +0 -103
  61. package/src/server/challenge.ts +26 -0
  62. package/src/server/create-wallet.ts +149 -0
  63. package/src/server/handlers.ts +93 -0
  64. package/src/server/index.ts +13 -0
  65. package/src/server/submit.ts +47 -0
  66. package/src/server/types.ts +70 -0
  67. package/src/server/utils.ts +69 -0
  68. package/src/types.ts +1 -0
  69. package/src/web.ts +51 -0
  70. package/tsconfig.json +6 -1
  71. package/tsup.config.ts +9 -1
@@ -0,0 +1,428 @@
1
+ import { bytesToHex } from '@thru/passkey-manager';
2
+ import { create } from 'zustand';
3
+ import { classifyPasskeyError } from '../mobile/errors';
4
+ import {
5
+ authenticateWithDiscoverablePasskey,
6
+ registerPasskey,
7
+ signWithPasskey,
8
+ } from '../mobile/passkey';
9
+ import {
10
+ clearPasskeyMetadata,
11
+ clearSession,
12
+ getStoredAddress,
13
+ getStoredPasskeyMetadata,
14
+ getStoredUserId,
15
+ hasStoredPasskey,
16
+ hasStoredWallet,
17
+ storePasskeyMetadata,
18
+ storeWalletInfo,
19
+ touchPasskeyLastUsedAt,
20
+ } from '../mobile/storage';
21
+ import type {
22
+ PasskeyAuthApiResponse,
23
+ PasskeyAuthBoundStore,
24
+ PasskeyAuthConfig,
25
+ PasskeyAuthStore,
26
+ PasskeyUser,
27
+ } from './types';
28
+
29
+ const storeCache = new Map<string, PasskeyAuthBoundStore<any>>();
30
+
31
+ function createStoreKey(config: PasskeyAuthConfig): string {
32
+ return [config.apiUrl, config.alias ?? '', config.rpId ?? '', config.rpName ?? ''].join('::');
33
+ }
34
+
35
+ function buildDisplayName(address: string): string {
36
+ return `${address.slice(0, 8)}...${address.slice(-4)}`;
37
+ }
38
+
39
+ function toPasskeyUser<TExtra>(
40
+ user: PasskeyAuthApiResponse<TExtra>['user']
41
+ ): PasskeyUser<TExtra> {
42
+ return {
43
+ id: user.id,
44
+ displayName: buildDisplayName(user.publicKey),
45
+ tokenAccountAddress: user.tokenAccountAddress ?? null,
46
+ extras: user.extras,
47
+ };
48
+ }
49
+
50
+ async function readJson<T>(response: Response): Promise<T> {
51
+ return (await response.json()) as T;
52
+ }
53
+
54
+ async function postJson<T>(
55
+ url: string,
56
+ body: Record<string, unknown>
57
+ ): Promise<T> {
58
+ const response = await fetch(url, {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify(body),
62
+ });
63
+
64
+ const data = await readJson<Record<string, unknown>>(response);
65
+ if (!response.ok || data.success !== true) {
66
+ throw new Error(
67
+ typeof data.error === 'string' ? data.error : 'Request failed'
68
+ );
69
+ }
70
+
71
+ return data as unknown as T;
72
+ }
73
+
74
+ async function getCurrentUser<TExtra>(
75
+ apiUrl: string,
76
+ address: string
77
+ ): Promise<PasskeyAuthApiResponse<TExtra> | null> {
78
+ const response = await fetch(`${apiUrl}/auth/me`, {
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ 'x-wallet-address': address,
82
+ },
83
+ });
84
+
85
+ if (response.status === 404) return null;
86
+
87
+ const data = await readJson<Record<string, unknown>>(response);
88
+ if (!response.ok || data.success !== true) {
89
+ throw new Error(
90
+ typeof data.error === 'string' ? data.error : 'Failed to fetch current user'
91
+ );
92
+ }
93
+
94
+ return data as unknown as PasskeyAuthApiResponse<TExtra>;
95
+ }
96
+
97
+ export function createPasskeyAuthStore<TExtra = Record<string, never>>(
98
+ config: PasskeyAuthConfig
99
+ ): PasskeyAuthBoundStore<TExtra> {
100
+ const resolvedAlias = config.alias ?? 'Thru Wallet';
101
+
102
+ return create<PasskeyAuthStore<TExtra>>((set, get) => ({
103
+ isAuthenticated: false,
104
+ isLoading: false,
105
+ isInitialized: false,
106
+ hasExistingPasskey: false,
107
+ needsNewPasskey: false,
108
+ error: null,
109
+ user: null,
110
+ address: null,
111
+ activeCredentialId: null,
112
+
113
+ initialize: async () => {
114
+ try {
115
+ const hasPasskey = await hasStoredPasskey();
116
+
117
+ if (hasPasskey) {
118
+ const storedAddress = await getStoredAddress();
119
+ if (storedAddress) {
120
+ const user = await Promise.race([
121
+ getCurrentUser<TExtra>(config.apiUrl, storedAddress),
122
+ new Promise<null>((_, reject) =>
123
+ setTimeout(() => reject(new Error('timeout')), 2000)
124
+ ),
125
+ ]).catch(() => undefined);
126
+
127
+ if (user === null) {
128
+ await clearSession();
129
+ await clearPasskeyMetadata();
130
+ set({ isInitialized: true, hasExistingPasskey: false });
131
+ return;
132
+ }
133
+ } else {
134
+ const metadata = await getStoredPasskeyMetadata();
135
+ if (metadata && !metadata.publicKeyX && !metadata.publicKeyY) {
136
+ await clearPasskeyMetadata();
137
+ set({ isInitialized: true, hasExistingPasskey: false });
138
+ return;
139
+ }
140
+ }
141
+ }
142
+
143
+ set({ isInitialized: true, hasExistingPasskey: hasPasskey });
144
+ } catch (error) {
145
+ console.error('Failed to initialize passkey auth:', error);
146
+ set({ isInitialized: true, error: 'Failed to initialize wallet' });
147
+ }
148
+ },
149
+
150
+ createWallet: async () => {
151
+ set({ isLoading: true, error: null, needsNewPasskey: false });
152
+
153
+ try {
154
+ const tempId = `user-${Date.now()}`;
155
+ const { credentialId, publicKeyX, publicKeyY, rpId } =
156
+ await registerPasskey(resolvedAlias, tempId, {
157
+ rpId: config.rpId,
158
+ rpName: config.rpName,
159
+ });
160
+
161
+ const now = new Date().toISOString();
162
+ const pubkeyXHex = bytesToHex(publicKeyX);
163
+ const pubkeyYHex = bytesToHex(publicKeyY);
164
+
165
+ await storePasskeyMetadata({
166
+ credentialId,
167
+ publicKeyX: pubkeyXHex,
168
+ publicKeyY: pubkeyYHex,
169
+ rpId,
170
+ createdAt: now,
171
+ lastUsedAt: now,
172
+ });
173
+
174
+ const response = await postJson<PasskeyAuthApiResponse<TExtra>>(
175
+ `${config.apiUrl}/auth/register-passkey-wallet`,
176
+ {
177
+ pubkeyX: pubkeyXHex,
178
+ pubkeyY: pubkeyYHex,
179
+ credentialId,
180
+ }
181
+ );
182
+
183
+ const walletAddress = response.user.publicKey;
184
+ await storeWalletInfo(
185
+ walletAddress,
186
+ response.user.id,
187
+ response.user.tokenAccountAddress ?? undefined
188
+ );
189
+
190
+ set({
191
+ isLoading: false,
192
+ isAuthenticated: true,
193
+ hasExistingPasskey: true,
194
+ activeCredentialId: credentialId,
195
+ address: walletAddress,
196
+ user: toPasskeyUser(response.user),
197
+ });
198
+
199
+ return true;
200
+ } catch (error) {
201
+ if (classifyPasskeyError(error) === 'USER_CANCELLED') {
202
+ set({ isLoading: false });
203
+ return false;
204
+ }
205
+
206
+ console.error('Failed to create passkey wallet:', error);
207
+ set({
208
+ isLoading: false,
209
+ error: error instanceof Error ? error.message : 'Failed to create wallet',
210
+ });
211
+ return false;
212
+ }
213
+ },
214
+
215
+ unlockWithPasskey: async () => {
216
+ set({ isLoading: true, error: null, needsNewPasskey: false });
217
+
218
+ try {
219
+ const metadata = await getStoredPasskeyMetadata();
220
+ if (!metadata) throw new Error('No stored passkey found');
221
+
222
+ await signWithPasskey(
223
+ metadata.credentialId,
224
+ crypto.getRandomValues(new Uint8Array(32)),
225
+ metadata.rpId
226
+ );
227
+ await touchPasskeyLastUsedAt().catch((error) => {
228
+ console.warn('Failed to update passkey last-used timestamp:', error);
229
+ });
230
+
231
+ const hasWallet = await hasStoredWallet();
232
+ let walletAddress: string;
233
+ let userId: string;
234
+ let tokenAccountAddress: string | undefined;
235
+ let response: PasskeyAuthApiResponse<TExtra> | null = null;
236
+
237
+ if (hasWallet) {
238
+ const storedAddress = await getStoredAddress();
239
+ const storedUserId = await getStoredUserId();
240
+ if (!storedAddress || !storedUserId) {
241
+ throw new Error('Incomplete wallet data');
242
+ }
243
+
244
+ walletAddress = storedAddress;
245
+ userId = storedUserId;
246
+
247
+ const current = await getCurrentUser<TExtra>(config.apiUrl, walletAddress);
248
+ if (current) {
249
+ response = current;
250
+ tokenAccountAddress = current.user.tokenAccountAddress ?? undefined;
251
+ } else if (metadata.publicKeyX && metadata.publicKeyY) {
252
+ response = await postJson<PasskeyAuthApiResponse<TExtra>>(
253
+ `${config.apiUrl}/auth/register-passkey-wallet`,
254
+ {
255
+ pubkeyX: metadata.publicKeyX,
256
+ pubkeyY: metadata.publicKeyY,
257
+ credentialId: metadata.credentialId,
258
+ }
259
+ );
260
+ walletAddress = response.user.publicKey;
261
+ userId = response.user.id;
262
+ tokenAccountAddress = response.user.tokenAccountAddress ?? undefined;
263
+ await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
264
+ } else {
265
+ await clearSession();
266
+ await clearPasskeyMetadata();
267
+ set({ isLoading: false, hasExistingPasskey: false, error: null });
268
+ return false;
269
+ }
270
+ } else if (metadata.publicKeyX && metadata.publicKeyY) {
271
+ response = await postJson<PasskeyAuthApiResponse<TExtra>>(
272
+ `${config.apiUrl}/auth/register-passkey-wallet`,
273
+ {
274
+ pubkeyX: metadata.publicKeyX,
275
+ pubkeyY: metadata.publicKeyY,
276
+ credentialId: metadata.credentialId,
277
+ }
278
+ );
279
+ walletAddress = response.user.publicKey;
280
+ userId = response.user.id;
281
+ tokenAccountAddress = response.user.tokenAccountAddress ?? undefined;
282
+ await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
283
+ } else {
284
+ const recovered = await postJson<PasskeyAuthApiResponse<TExtra>>(
285
+ `${config.apiUrl}/auth/recover-passkey-wallet`,
286
+ { credentialId: metadata.credentialId }
287
+ ).catch(() => null);
288
+
289
+ if (!recovered) {
290
+ await clearSession();
291
+ await clearPasskeyMetadata();
292
+ set({ isLoading: false, hasExistingPasskey: false, error: null });
293
+ return false;
294
+ }
295
+
296
+ response = recovered;
297
+ walletAddress = recovered.user.publicKey;
298
+ userId = recovered.user.id;
299
+ tokenAccountAddress = recovered.user.tokenAccountAddress ?? undefined;
300
+ await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
301
+ }
302
+
303
+ set({
304
+ isLoading: false,
305
+ isAuthenticated: true,
306
+ activeCredentialId: metadata.credentialId,
307
+ address: walletAddress,
308
+ user: response ? toPasskeyUser(response.user) : get().user,
309
+ });
310
+
311
+ return true;
312
+ } catch (error) {
313
+ console.error('Failed to unlock with passkey:', error);
314
+ const kind = classifyPasskeyError(error);
315
+
316
+ if (kind === 'USER_CANCELLED') {
317
+ set({ isLoading: false });
318
+ } else if (kind === 'NOT_FOUND') {
319
+ await clearPasskeyMetadata();
320
+ set({ isLoading: false, hasExistingPasskey: false, error: null });
321
+ } else {
322
+ set({
323
+ isLoading: false,
324
+ error: error instanceof Error ? error.message : 'Failed to unlock',
325
+ });
326
+ }
327
+
328
+ return false;
329
+ }
330
+ },
331
+
332
+ recoverWithDiscoverablePasskey: async () => {
333
+ set({ isLoading: true, error: null, needsNewPasskey: false });
334
+
335
+ try {
336
+ const discovered = await authenticateWithDiscoverablePasskey({
337
+ rpId: config.rpId,
338
+ });
339
+
340
+ if (!discovered) {
341
+ set({ isLoading: false });
342
+ return false;
343
+ }
344
+
345
+ const response = await postJson<PasskeyAuthApiResponse<TExtra>>(
346
+ `${config.apiUrl}/auth/recover-passkey-wallet`,
347
+ { credentialId: discovered.credentialId }
348
+ ).catch(() => null);
349
+
350
+ if (!response) {
351
+ set({ isLoading: false, needsNewPasskey: true });
352
+ return false;
353
+ }
354
+
355
+ const walletAddress = response.user.publicKey;
356
+ const userId = response.user.id;
357
+ const tokenAccountAddress = response.user.tokenAccountAddress ?? undefined;
358
+ const now = new Date().toISOString();
359
+
360
+ await storePasskeyMetadata({
361
+ credentialId: discovered.credentialId,
362
+ publicKeyX: '',
363
+ publicKeyY: '',
364
+ rpId: discovered.rpId,
365
+ createdAt: now,
366
+ lastUsedAt: now,
367
+ });
368
+ await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
369
+
370
+ set({
371
+ isLoading: false,
372
+ isAuthenticated: true,
373
+ hasExistingPasskey: true,
374
+ activeCredentialId: discovered.credentialId,
375
+ address: walletAddress,
376
+ user: toPasskeyUser(response.user),
377
+ });
378
+
379
+ return true;
380
+ } catch (error) {
381
+ console.error('Failed to recover with discoverable passkey:', error);
382
+ set({
383
+ isLoading: false,
384
+ error:
385
+ error instanceof Error ? error.message : 'Failed to recover wallet',
386
+ });
387
+ return false;
388
+ }
389
+ },
390
+
391
+ logout: async () => {
392
+ try {
393
+ await clearSession();
394
+ } catch (error) {
395
+ console.error('Failed to clear passkey session:', error);
396
+ }
397
+
398
+ set({
399
+ isAuthenticated: false,
400
+ needsNewPasskey: false,
401
+ user: null,
402
+ address: null,
403
+ activeCredentialId: null,
404
+ });
405
+ },
406
+
407
+ clearError: () => set({ error: null }),
408
+ dismissNewPasskey: () => set({ needsNewPasskey: false }),
409
+ }));
410
+ }
411
+
412
+ export function getPasskeyAuthStore<TExtra = Record<string, never>>(
413
+ config: PasskeyAuthConfig
414
+ ): PasskeyAuthBoundStore<TExtra> {
415
+ const key = createStoreKey(config);
416
+ const cached = storeCache.get(key) as PasskeyAuthBoundStore<TExtra> | undefined;
417
+ if (cached) return cached;
418
+
419
+ const store = createPasskeyAuthStore<TExtra>(config);
420
+ storeCache.set(key, store);
421
+ return store;
422
+ }
423
+
424
+ export function usePasskeyAuth<TExtra = Record<string, never>>(
425
+ config: PasskeyAuthConfig
426
+ ): PasskeyAuthStore<TExtra> {
427
+ return getPasskeyAuthStore<TExtra>(config)();
428
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
- // Types
1
+ /**
2
+ * @deprecated Import browser APIs from `@thru/passkey/web` and popup APIs from
3
+ * `@thru/passkey/popup`. The root export path remains as a temporary
4
+ * compatibility shim and will be removed after downstream consumers migrate.
5
+ */
2
6
  export type {
3
7
  PasskeyRegistrationResult,
4
8
  PasskeySigningResult,
@@ -8,27 +12,16 @@ export type {
8
12
  PasskeyClientCapabilities,
9
13
  PasskeyPopupContext,
10
14
  PasskeyPopupAccount,
11
- PasskeyPopupAction,
12
- PasskeyPopupGetRequestPayload,
13
- PasskeyPopupCreateRequestPayload,
14
- PasskeyPopupGetStoredRequestPayload,
15
- PasskeyPopupRequestPayload,
16
- PasskeyPopupRequest,
17
- PasskeyPopupSigningResult,
18
- PasskeyPopupStoredPasskey,
19
- PasskeyPopupStoredSigningResult,
20
- PasskeyPopupRegistrationResult,
21
- PasskeyPopupResponse,
22
- } from './types';
23
-
24
- // Registration
25
- export { registerPasskey } from './register';
26
-
27
- // Signing
28
- export { signWithPasskey, signWithStoredPasskey, signWithDiscoverablePasskey } from './sign';
15
+ } from './web';
29
16
 
30
- // Crypto (re-exported from @thru/passkey-manager)
17
+ /**
18
+ * @deprecated Import browser APIs from `@thru/passkey/web`.
19
+ */
31
20
  export {
21
+ registerPasskey,
22
+ signWithPasskey,
23
+ signWithStoredPasskey,
24
+ signWithDiscoverablePasskey,
32
25
  parseDerSignature,
33
26
  normalizeLowS,
34
27
  normalizeSignatureComponent,
@@ -36,23 +29,15 @@ export {
36
29
  P256_HALF_N,
37
30
  bytesToBigIntBE,
38
31
  bigIntToBytesBE,
39
- } from '@thru/passkey-manager';
40
-
41
- // Capabilities
42
- export {
43
32
  isWebAuthnSupported,
44
33
  preloadPasskeyClientCapabilities,
45
34
  getPasskeyClientCapabilities,
46
35
  getCachedPasskeyClientCapabilities,
47
36
  shouldUsePasskeyPopup,
48
37
  isInIframe,
49
- type PasskeyPromptAction,
50
- } from './capabilities';
51
-
52
- // Encoding (re-exported from @thru/passkey-manager)
53
- export {
54
38
  arrayBufferToBase64Url,
55
39
  base64UrlToArrayBuffer,
40
+ bytesToBase64,
56
41
  bytesToBase64Url,
57
42
  base64UrlToBytes,
58
43
  bytesToHex,
@@ -60,9 +45,29 @@ export {
60
45
  bytesEqual,
61
46
  compareBytes,
62
47
  uniqueAccounts,
63
- } from '@thru/passkey-manager';
48
+ type PasskeyPromptAction,
49
+ } from './web';
64
50
 
65
- // Popup (parent side)
51
+ /**
52
+ * @deprecated Import popup APIs from `@thru/passkey/popup`.
53
+ */
54
+ export type {
55
+ PasskeyPopupAction,
56
+ PasskeyPopupGetRequestPayload,
57
+ PasskeyPopupCreateRequestPayload,
58
+ PasskeyPopupGetStoredRequestPayload,
59
+ PasskeyPopupRequestPayload,
60
+ PasskeyPopupRequest,
61
+ PasskeyPopupSigningResult,
62
+ PasskeyPopupStoredPasskey,
63
+ PasskeyPopupStoredSigningResult,
64
+ PasskeyPopupRegistrationResult,
65
+ PasskeyPopupResponse,
66
+ } from './popup-entry';
67
+
68
+ /**
69
+ * @deprecated Import popup APIs from `@thru/passkey/popup`.
70
+ */
66
71
  export {
67
72
  PASSKEY_POPUP_PATH,
68
73
  PASSKEY_POPUP_READY_EVENT,
@@ -72,15 +77,8 @@ export {
72
77
  openPasskeyPopupWindow,
73
78
  closePopup,
74
79
  requestPasskeyPopup,
75
- } from './popup';
76
-
77
- // Popup service (popup window side)
78
- export {
79
80
  toPopupSigningResult,
80
81
  buildSuccessResponse,
81
82
  decodeChallenge,
82
- getPopupDisplayInfo,
83
83
  getResponseError,
84
- signWithPreferredPasskey,
85
- buildStoredPasskeyResult,
86
- } from './popup-service';
84
+ } from './popup-entry';
@@ -0,0 +1,31 @@
1
+ const PASSKEY_ERRORS = {
2
+ USER_CANCELLED: [
3
+ 'error 1001',
4
+ 'UserCancelled',
5
+ 'Passkey authentication was cancelled',
6
+ 'Passkey registration was cancelled',
7
+ ],
8
+ NOT_FOUND: [
9
+ 'not found',
10
+ 'No credentials available',
11
+ 'no passkey',
12
+ 'NoCredentials',
13
+ ],
14
+ } as const;
15
+
16
+ export type PasskeyErrorKind = keyof typeof PASSKEY_ERRORS;
17
+
18
+ export function classifyPasskeyError(error: unknown): PasskeyErrorKind | null {
19
+ const message =
20
+ error instanceof Error ? error.message : typeof error === 'string' ? error : null;
21
+
22
+ if (!message) return null;
23
+
24
+ for (const [kind, patterns] of Object.entries(PASSKEY_ERRORS)) {
25
+ if (patterns.some((pattern) => message.includes(pattern))) {
26
+ return kind as PasskeyErrorKind;
27
+ }
28
+ }
29
+
30
+ return null;
31
+ }
@@ -0,0 +1,33 @@
1
+ export type {
2
+ PasskeyMetadata,
3
+ PasskeySigningResult,
4
+ PasskeyMobileConfig,
5
+ PasskeyRegistrationResult,
6
+ DiscoverablePasskeyResult,
7
+ StoredPasskeySigningResult,
8
+ } from './types';
9
+
10
+ export { classifyPasskeyError, type PasskeyErrorKind } from './errors';
11
+
12
+ export { bytesToBase64 } from '@thru/passkey-manager';
13
+
14
+ export {
15
+ storePasskeyMetadata,
16
+ touchPasskeyLastUsedAt,
17
+ getStoredPasskeyMetadata,
18
+ hasStoredPasskey,
19
+ clearPasskeyMetadata,
20
+ storeWalletInfo,
21
+ getStoredAddress,
22
+ getStoredUserId,
23
+ getStoredTokenAccount,
24
+ hasStoredWallet,
25
+ clearSession,
26
+ } from './storage';
27
+
28
+ export {
29
+ registerPasskey,
30
+ signWithPasskey,
31
+ authenticateWithDiscoverablePasskey,
32
+ extractP256Coordinates,
33
+ } from './passkey';