@thru/passkey 0.2.13 → 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,154 @@
1
+ import { create as passkeyCreate, get as passkeyGet } from 'react-native-passkeys';
2
+ import {
3
+ bytesToBase64Url,
4
+ base64UrlToBytes,
5
+ normalizeLowS,
6
+ parseDerSignature,
7
+ type PasskeySigningResult,
8
+ } from '@thru/passkey-manager';
9
+ import type {
10
+ DiscoverablePasskeyResult,
11
+ PasskeyMobileConfig,
12
+ PasskeyRegistrationResult,
13
+ } from './types';
14
+
15
+ type ProcessLike = typeof globalThis & {
16
+ process?: {
17
+ env?: Record<string, string | undefined>;
18
+ };
19
+ };
20
+
21
+ function getDefaultConfig(config?: PasskeyMobileConfig): Required<PasskeyMobileConfig> {
22
+ const env = (globalThis as ProcessLike).process?.env ?? {};
23
+
24
+ return {
25
+ rpId: config?.rpId ?? env.EXPO_PUBLIC_PASSKEY_RP_ID ?? 'wallet.thru.org',
26
+ rpName: config?.rpName ?? env.EXPO_PUBLIC_PASSKEY_RP_NAME ?? 'Thru Wallet',
27
+ };
28
+ }
29
+
30
+ export async function registerPasskey(
31
+ alias: string,
32
+ userId: string,
33
+ config?: PasskeyMobileConfig
34
+ ): Promise<PasskeyRegistrationResult> {
35
+ const { rpId, rpName } = getDefaultConfig(config);
36
+ const challenge = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
37
+ const userIdB64 = bytesToBase64Url(new TextEncoder().encode(userId));
38
+
39
+ const result = await passkeyCreate({
40
+ challenge,
41
+ rp: { id: rpId, name: rpName },
42
+ user: { id: userIdB64, name: alias, displayName: alias },
43
+ pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
44
+ authenticatorSelection: {
45
+ authenticatorAttachment: 'platform',
46
+ userVerification: 'required',
47
+ residentKey: 'required',
48
+ },
49
+ attestation: 'none',
50
+ timeout: 60000,
51
+ });
52
+
53
+ if (!result) {
54
+ throw new Error('Passkey registration was cancelled');
55
+ }
56
+
57
+ const publicKeyB64 = result.response.getPublicKey?.();
58
+ if (!publicKeyB64) {
59
+ throw new Error('Failed to retrieve public key from registration');
60
+ }
61
+
62
+ const keyBytes = base64UrlToBytes(publicKeyB64);
63
+ const { x, y } = extractP256Coordinates(keyBytes);
64
+
65
+ return {
66
+ credentialId: result.id,
67
+ publicKeyX: x,
68
+ publicKeyY: y,
69
+ rpId,
70
+ };
71
+ }
72
+
73
+ export async function signWithPasskey(
74
+ credentialId: string,
75
+ challenge: Uint8Array,
76
+ rpId?: string
77
+ ): Promise<PasskeySigningResult> {
78
+ const resolvedRpId = rpId ?? getDefaultConfig().rpId;
79
+ const challengeB64 = bytesToBase64Url(challenge);
80
+
81
+ const result = await passkeyGet({
82
+ challenge: challengeB64,
83
+ rpId: resolvedRpId,
84
+ allowCredentials: [{ type: 'public-key', id: credentialId }],
85
+ userVerification: 'required',
86
+ timeout: 60000,
87
+ });
88
+
89
+ if (!result) {
90
+ throw new Error('Passkey authentication was cancelled');
91
+ }
92
+
93
+ const derSignature = base64UrlToBytes(result.response.signature);
94
+ let { r, s } = parseDerSignature(derSignature);
95
+ s = normalizeLowS(s);
96
+
97
+ return {
98
+ signature: new Uint8Array([...r, ...s]),
99
+ authenticatorData: base64UrlToBytes(result.response.authenticatorData),
100
+ clientDataJSON: base64UrlToBytes(result.response.clientDataJSON),
101
+ signatureR: r,
102
+ signatureS: s,
103
+ };
104
+ }
105
+
106
+ export async function authenticateWithDiscoverablePasskey(
107
+ config?: Pick<PasskeyMobileConfig, 'rpId'>
108
+ ): Promise<DiscoverablePasskeyResult | null> {
109
+ const { rpId } = getDefaultConfig(config);
110
+
111
+ try {
112
+ const challenge = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
113
+ const result = await passkeyGet({
114
+ challenge,
115
+ rpId,
116
+ userVerification: 'required',
117
+ timeout: 60000,
118
+ });
119
+
120
+ return result ? { credentialId: result.id, rpId } : null;
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ export function extractP256Coordinates(
127
+ keyBytes: Uint8Array
128
+ ): { x: Uint8Array; y: Uint8Array } {
129
+ if (keyBytes.length === 64) {
130
+ return {
131
+ x: keyBytes.slice(0, 32),
132
+ y: keyBytes.slice(32, 64),
133
+ };
134
+ }
135
+
136
+ if (keyBytes.length === 65 && keyBytes[0] === 0x04) {
137
+ return {
138
+ x: keyBytes.slice(1, 33),
139
+ y: keyBytes.slice(33, 65),
140
+ };
141
+ }
142
+
143
+ const pointStart = keyBytes.length - 65;
144
+ if (pointStart > 0 && keyBytes[pointStart] === 0x04) {
145
+ return {
146
+ x: keyBytes.slice(pointStart + 1, pointStart + 33),
147
+ y: keyBytes.slice(pointStart + 33, pointStart + 65),
148
+ };
149
+ }
150
+
151
+ throw new Error(
152
+ `Unsupported public key format (${keyBytes.length} bytes). Expected raw X||Y (64), uncompressed point (65), or SPKI DER (91).`
153
+ );
154
+ }
@@ -0,0 +1,115 @@
1
+ import * as SecureStore from 'expo-secure-store';
2
+ import type { PasskeyMetadata } from '@thru/passkey-manager';
3
+
4
+ const SECURE_STORE_OPTS = {
5
+ keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
6
+ } as const;
7
+
8
+ const PASSKEY_CREDENTIAL_ID_KEY = 'thru_passkey_credential_id';
9
+ const PASSKEY_PUBLIC_KEY_X_KEY = 'thru_passkey_pubkey_x';
10
+ const PASSKEY_PUBLIC_KEY_Y_KEY = 'thru_passkey_pubkey_y';
11
+ const PASSKEY_RP_ID_KEY = 'thru_passkey_rp_id';
12
+ const PASSKEY_LABEL_KEY = 'thru_passkey_label';
13
+ const PASSKEY_CREATED_AT_KEY = 'thru_passkey_created_at';
14
+ const PASSKEY_LAST_USED_AT_KEY = 'thru_passkey_last_used_at';
15
+
16
+ const ADDRESS_KEY = 'thru_address';
17
+ const USER_ID_KEY = 'thru_user_id';
18
+ const TOKEN_ACCOUNT_KEY = 'thru_token_account';
19
+
20
+ export async function storePasskeyMetadata(metadata: PasskeyMetadata): Promise<void> {
21
+ await Promise.all([
22
+ SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, metadata.credentialId, SECURE_STORE_OPTS),
23
+ SecureStore.setItemAsync(PASSKEY_PUBLIC_KEY_X_KEY, metadata.publicKeyX, SECURE_STORE_OPTS),
24
+ SecureStore.setItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY, metadata.publicKeyY, SECURE_STORE_OPTS),
25
+ SecureStore.setItemAsync(PASSKEY_RP_ID_KEY, metadata.rpId, SECURE_STORE_OPTS),
26
+ SecureStore.setItemAsync(PASSKEY_LABEL_KEY, metadata.label ?? '', SECURE_STORE_OPTS),
27
+ SecureStore.setItemAsync(PASSKEY_CREATED_AT_KEY, metadata.createdAt, SECURE_STORE_OPTS),
28
+ SecureStore.setItemAsync(PASSKEY_LAST_USED_AT_KEY, metadata.lastUsedAt, SECURE_STORE_OPTS),
29
+ ]);
30
+ }
31
+
32
+ export async function getStoredPasskeyMetadata(): Promise<PasskeyMetadata | null> {
33
+ const credentialId = await SecureStore.getItemAsync(PASSKEY_CREDENTIAL_ID_KEY);
34
+ if (!credentialId) return null;
35
+
36
+ const [publicKeyX, publicKeyY, rpId, label, createdAt, lastUsedAt] = await Promise.all([
37
+ SecureStore.getItemAsync(PASSKEY_PUBLIC_KEY_X_KEY),
38
+ SecureStore.getItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY),
39
+ SecureStore.getItemAsync(PASSKEY_RP_ID_KEY),
40
+ SecureStore.getItemAsync(PASSKEY_LABEL_KEY),
41
+ SecureStore.getItemAsync(PASSKEY_CREATED_AT_KEY),
42
+ SecureStore.getItemAsync(PASSKEY_LAST_USED_AT_KEY),
43
+ ]);
44
+
45
+ if (!rpId || !createdAt) return null;
46
+
47
+ return {
48
+ credentialId,
49
+ publicKeyX: publicKeyX ?? '',
50
+ publicKeyY: publicKeyY ?? '',
51
+ rpId,
52
+ label: label || undefined,
53
+ createdAt,
54
+ lastUsedAt: lastUsedAt ?? createdAt,
55
+ };
56
+ }
57
+
58
+ export async function touchPasskeyLastUsedAt(lastUsedAt = new Date().toISOString()): Promise<string> {
59
+ await SecureStore.setItemAsync(PASSKEY_LAST_USED_AT_KEY, lastUsedAt, SECURE_STORE_OPTS);
60
+ return lastUsedAt;
61
+ }
62
+
63
+ export async function hasStoredPasskey(): Promise<boolean> {
64
+ return (await SecureStore.getItemAsync(PASSKEY_CREDENTIAL_ID_KEY)) !== null;
65
+ }
66
+
67
+ export async function clearPasskeyMetadata(): Promise<void> {
68
+ await Promise.all([
69
+ SecureStore.deleteItemAsync(PASSKEY_CREDENTIAL_ID_KEY),
70
+ SecureStore.deleteItemAsync(PASSKEY_PUBLIC_KEY_X_KEY),
71
+ SecureStore.deleteItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY),
72
+ SecureStore.deleteItemAsync(PASSKEY_RP_ID_KEY),
73
+ SecureStore.deleteItemAsync(PASSKEY_LABEL_KEY),
74
+ SecureStore.deleteItemAsync(PASSKEY_CREATED_AT_KEY),
75
+ SecureStore.deleteItemAsync(PASSKEY_LAST_USED_AT_KEY),
76
+ ]);
77
+ }
78
+
79
+ export async function storeWalletInfo(
80
+ address: string,
81
+ userId: string,
82
+ tokenAccountAddress?: string
83
+ ): Promise<void> {
84
+ await Promise.all([
85
+ SecureStore.setItemAsync(ADDRESS_KEY, address, SECURE_STORE_OPTS),
86
+ SecureStore.setItemAsync(USER_ID_KEY, userId, SECURE_STORE_OPTS),
87
+ tokenAccountAddress
88
+ ? SecureStore.setItemAsync(TOKEN_ACCOUNT_KEY, tokenAccountAddress, SECURE_STORE_OPTS)
89
+ : SecureStore.deleteItemAsync(TOKEN_ACCOUNT_KEY),
90
+ ]);
91
+ }
92
+
93
+ export async function hasStoredWallet(): Promise<boolean> {
94
+ return (await SecureStore.getItemAsync(ADDRESS_KEY)) !== null;
95
+ }
96
+
97
+ export async function getStoredAddress(): Promise<string | null> {
98
+ return SecureStore.getItemAsync(ADDRESS_KEY);
99
+ }
100
+
101
+ export async function getStoredUserId(): Promise<string | null> {
102
+ return SecureStore.getItemAsync(USER_ID_KEY);
103
+ }
104
+
105
+ export async function getStoredTokenAccount(): Promise<string | null> {
106
+ return SecureStore.getItemAsync(TOKEN_ACCOUNT_KEY);
107
+ }
108
+
109
+ export async function clearSession(): Promise<void> {
110
+ await Promise.all([
111
+ SecureStore.deleteItemAsync(ADDRESS_KEY),
112
+ SecureStore.deleteItemAsync(USER_ID_KEY),
113
+ SecureStore.deleteItemAsync(TOKEN_ACCOUNT_KEY),
114
+ ]);
115
+ }
@@ -0,0 +1,24 @@
1
+ import type { PasskeySigningResult, PasskeyMetadata } from '@thru/passkey-manager';
2
+
3
+ export type { PasskeySigningResult, PasskeyMetadata } from '@thru/passkey-manager';
4
+
5
+ export interface PasskeyMobileConfig {
6
+ rpId?: string;
7
+ rpName?: string;
8
+ }
9
+
10
+ export interface PasskeyRegistrationResult {
11
+ credentialId: string;
12
+ publicKeyX: Uint8Array;
13
+ publicKeyY: Uint8Array;
14
+ rpId: string;
15
+ }
16
+
17
+ export interface DiscoverablePasskeyResult {
18
+ credentialId: string;
19
+ rpId: string;
20
+ }
21
+
22
+ export interface StoredPasskeySigningResult extends PasskeySigningResult {
23
+ passkey: PasskeyMetadata;
24
+ }
@@ -0,0 +1,33 @@
1
+ export type {
2
+ PasskeyPopupAction,
3
+ PasskeyPopupGetRequestPayload,
4
+ PasskeyPopupCreateRequestPayload,
5
+ PasskeyPopupGetStoredRequestPayload,
6
+ PasskeyPopupRequestPayload,
7
+ PasskeyPopupRequest,
8
+ PasskeyPopupSigningResult,
9
+ PasskeyPopupStoredPasskey,
10
+ PasskeyPopupStoredSigningResult,
11
+ PasskeyPopupRegistrationResult,
12
+ PasskeyPopupResponse,
13
+ PasskeyPopupContext,
14
+ PasskeyPopupAccount,
15
+ } from './types';
16
+
17
+ export {
18
+ PASSKEY_POPUP_PATH,
19
+ PASSKEY_POPUP_READY_EVENT,
20
+ PASSKEY_POPUP_REQUEST_EVENT,
21
+ PASSKEY_POPUP_RESPONSE_EVENT,
22
+ PASSKEY_POPUP_CHANNEL,
23
+ openPasskeyPopupWindow,
24
+ closePopup,
25
+ requestPasskeyPopup,
26
+ } from './popup';
27
+
28
+ export {
29
+ toPopupSigningResult,
30
+ buildSuccessResponse,
31
+ decodeChallenge,
32
+ getResponseError,
33
+ } from './popup-service';
@@ -1,24 +1,13 @@
1
1
  import type {
2
2
  PasskeyPopupAction,
3
- PasskeyPopupAccount,
4
- PasskeyPopupContext,
5
3
  PasskeyPopupResponse,
6
4
  PasskeyPopupSigningResult,
7
- PasskeyPopupStoredPasskey,
8
- PasskeyPopupStoredSigningResult,
9
- PasskeyMetadata,
10
5
  PasskeySigningResult,
11
6
  } from './types';
12
7
  import {
13
8
  PASSKEY_POPUP_RESPONSE_EVENT,
14
9
  } from './popup';
15
10
  import { bytesToBase64Url, base64UrlToBytes } from '@thru/passkey-manager';
16
- import { signWithPasskey, signWithDiscoverablePasskey } from './sign';
17
-
18
- type PasskeySignResult =
19
- | Awaited<ReturnType<typeof signWithDiscoverablePasskey>>
20
- | Awaited<ReturnType<typeof signWithPasskey>>;
21
-
22
11
  export function toPopupSigningResult(result: PasskeySigningResult): PasskeyPopupSigningResult {
23
12
  return {
24
13
  signatureBase64Url: bytesToBase64Url(result.signature),
@@ -47,23 +36,6 @@ export function decodeChallenge(base64Url: string): Uint8Array {
47
36
  return base64UrlToBytes(base64Url);
48
37
  }
49
38
 
50
- export function getPopupDisplayInfo(context?: PasskeyPopupContext): {
51
- name: string;
52
- url?: string;
53
- imageUrl?: string;
54
- logoText: string;
55
- } {
56
- const name = context?.appName || context?.origin || 'A dApp';
57
- const url = context?.appUrl || context?.origin;
58
- const logoText = name.charAt(0).toUpperCase() || 'A';
59
- return {
60
- name,
61
- url,
62
- imageUrl: context?.imageUrl,
63
- logoText,
64
- };
65
- }
66
-
67
39
  export function getResponseError(action: PasskeyPopupAction, error: unknown): { name?: string; message: string } {
68
40
  const { name, message } = normalizeError(error);
69
41
  const actionLabel = `Popup ${action}`;
@@ -76,72 +48,6 @@ export function getResponseError(action: PasskeyPopupAction, error: unknown): {
76
48
  message: detailedMessage,
77
49
  };
78
50
  }
79
-
80
- export async function signWithPreferredPasskey(
81
- preferredPasskey: PasskeyMetadata | null,
82
- challenge: Uint8Array,
83
- log?: (message: string) => void
84
- ): Promise<{ result: PasskeySignResult; credentialId: string; rpId: string }> {
85
- const resolvedRpId = preferredPasskey?.rpId ?? window.location.hostname;
86
-
87
- if (preferredPasskey?.credentialId && preferredPasskey.rpId) {
88
- try {
89
- const storedResult = await signWithPasskey(
90
- preferredPasskey.credentialId,
91
- challenge,
92
- preferredPasskey.rpId
93
- );
94
- return {
95
- result: storedResult,
96
- credentialId: preferredPasskey.credentialId,
97
- rpId: preferredPasskey.rpId,
98
- };
99
- } catch (error) {
100
- if (!shouldFallbackToDiscoverable(error)) {
101
- throw error;
102
- }
103
- if (log) {
104
- log('stored passkey failed; falling back to discoverable prompt');
105
- }
106
- }
107
- }
108
-
109
- const discovered = await signWithDiscoverablePasskey(challenge, resolvedRpId);
110
- return {
111
- result: discovered,
112
- credentialId: discovered.credentialId,
113
- rpId: resolvedRpId,
114
- };
115
- }
116
-
117
- export function buildStoredPasskeyResult(
118
- signed: { result: PasskeySignResult; credentialId: string; rpId: string },
119
- preferredPasskey: PasskeyMetadata | null,
120
- profiles: Array<{ passkey: PasskeyMetadata | null }>,
121
- accounts: PasskeyPopupAccount[]
122
- ): PasskeyPopupStoredSigningResult {
123
- const now = new Date().toISOString();
124
- const matchingPasskey =
125
- profiles.find((profile) => profile.passkey?.credentialId === signed.credentialId)?.passkey ??
126
- null;
127
-
128
- const passkey: PasskeyPopupStoredPasskey = (matchingPasskey ?? {
129
- credentialId: signed.credentialId,
130
- publicKeyX: '',
131
- publicKeyY: '',
132
- rpId: signed.rpId,
133
- label: preferredPasskey?.label,
134
- createdAt: now,
135
- lastUsedAt: now,
136
- }) as PasskeyPopupStoredPasskey;
137
-
138
- return {
139
- ...toPopupSigningResult(signed.result),
140
- passkey: matchingPasskey ? { ...passkey, lastUsedAt: now } : passkey,
141
- accounts,
142
- };
143
- }
144
-
145
51
  function normalizeError(error: unknown): { name?: string; message?: string; normalized: string } {
146
52
  const name =
147
53
  error && typeof error === 'object' && 'name' in error
@@ -157,12 +63,3 @@ function normalizeError(error: unknown): { name?: string; message?: string; norm
157
63
  normalized: `${name} ${message}`.toLowerCase(),
158
64
  };
159
65
  }
160
-
161
- function shouldFallbackToDiscoverable(error: unknown): boolean {
162
- const normalized = normalizeError(error).normalized;
163
- return (
164
- normalized.includes('notfounderror') ||
165
- normalized.includes('notallowederror') ||
166
- normalized.includes('securityerror')
167
- );
168
- }
@@ -0,0 +1,26 @@
1
+ import {
2
+ bytesToBase64Url,
3
+ createValidateChallenge,
4
+ fetchWalletNonce,
5
+ } from '@thru/passkey-manager';
6
+ import type { AccountContext } from '@thru/passkey-manager';
7
+ import type { PasskeyChallengeResult, ThruClient } from './types';
8
+
9
+ export async function createPasskeyChallenge(opts: {
10
+ client: ThruClient;
11
+ walletAddress: string;
12
+ accountCtx: AccountContext;
13
+ invokeIx: Uint8Array;
14
+ }): Promise<PasskeyChallengeResult> {
15
+ const nonce = await fetchWalletNonce(opts.client, opts.walletAddress);
16
+ const challenge = await createValidateChallenge(
17
+ nonce,
18
+ opts.accountCtx.accountAddresses,
19
+ opts.invokeIx
20
+ );
21
+
22
+ return {
23
+ challenge: bytesToBase64Url(challenge),
24
+ nonce: nonce.toString(),
25
+ };
26
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ PASSKEY_MANAGER_PROGRAM_ADDRESS,
3
+ base64UrlToBytes,
4
+ buildAccountContext,
5
+ createCredentialLookupSeed,
6
+ createWalletSeed,
7
+ deriveCredentialLookupAddress,
8
+ deriveWalletAddress,
9
+ encodeCreateInstruction,
10
+ encodeRegisterCredentialInstruction,
11
+ } from '@thru/passkey-manager';
12
+ import { toThruAddress, getStateProof, trackTransaction } from './utils';
13
+ import type { ThruClient } from './types';
14
+
15
+ export async function createPasskeyWallet(opts: {
16
+ client: ThruClient;
17
+ adminPublicKey: Uint8Array;
18
+ adminPrivateKey: string;
19
+ adminAddress: string;
20
+ pubkeyX: Uint8Array;
21
+ pubkeyY: Uint8Array;
22
+ credentialId?: string;
23
+ walletName?: string;
24
+ }): Promise<{ walletAddress: string; credentialLookupAddress?: string }> {
25
+ const walletName = opts.walletName ?? 'default';
26
+ const seed = await createWalletSeed(walletName, opts.pubkeyX, opts.pubkeyY);
27
+ const walletBytes = await deriveWalletAddress(seed, PASSKEY_MANAGER_PROGRAM_ADDRESS);
28
+ const walletAddress = toThruAddress(walletBytes);
29
+
30
+ let walletExists = false;
31
+ try {
32
+ await opts.client.accounts.get(walletAddress);
33
+ walletExists = true;
34
+ } catch {
35
+ walletExists = false;
36
+ }
37
+
38
+ if (!walletExists) {
39
+ const stateProof = await getStateProof(opts.client, walletAddress);
40
+ const accountCtx = buildAccountContext({
41
+ walletAddress,
42
+ readWriteAccounts: [],
43
+ readOnlyAccounts: [],
44
+ feePayerAddress: opts.adminAddress,
45
+ programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,
46
+ });
47
+
48
+ const createIx = encodeCreateInstruction({
49
+ walletAccountIdx: accountCtx.walletAccountIdx,
50
+ authority: {
51
+ tag: 1,
52
+ pubkeyX: opts.pubkeyX,
53
+ pubkeyY: opts.pubkeyY,
54
+ },
55
+ seed,
56
+ stateProof,
57
+ });
58
+
59
+ const transaction = await opts.client.transactions.build({
60
+ feePayer: { publicKey: opts.adminPublicKey },
61
+ program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
62
+ instructionData: createIx,
63
+ accounts: {
64
+ readWrite: [walletAddress],
65
+ readOnly: [],
66
+ },
67
+ header: { fee: 0n },
68
+ });
69
+
70
+ await transaction.sign(opts.adminPrivateKey);
71
+ const signature = await opts.client.transactions.send(transaction.toWire());
72
+ const result = await trackTransaction(opts.client, signature, 60000);
73
+ if (result.status !== 'finalized') {
74
+ throw new Error(
75
+ `Wallet creation failed with error code: ${result.errorCode ?? 'unknown'}`
76
+ );
77
+ }
78
+ }
79
+
80
+ let credentialLookupAddress: string | undefined;
81
+ if (opts.credentialId) {
82
+ const credentialIdBytes = base64UrlToBytes(opts.credentialId);
83
+ const lookupAddressBytes = await deriveCredentialLookupAddress(
84
+ credentialIdBytes,
85
+ walletName,
86
+ PASSKEY_MANAGER_PROGRAM_ADDRESS
87
+ );
88
+
89
+ credentialLookupAddress = toThruAddress(lookupAddressBytes);
90
+
91
+ let lookupExists = false;
92
+ try {
93
+ await opts.client.accounts.get(credentialLookupAddress);
94
+ lookupExists = true;
95
+ } catch {
96
+ lookupExists = false;
97
+ }
98
+
99
+ if (!lookupExists) {
100
+ try {
101
+ const credSeed = await createCredentialLookupSeed(credentialIdBytes, walletName);
102
+ const stateProof = await getStateProof(opts.client, credentialLookupAddress);
103
+ const accountCtx = buildAccountContext({
104
+ walletAddress,
105
+ readWriteAccounts: [lookupAddressBytes],
106
+ readOnlyAccounts: [],
107
+ feePayerAddress: opts.adminAddress,
108
+ programAddress: PASSKEY_MANAGER_PROGRAM_ADDRESS,
109
+ });
110
+
111
+ const registerIx = encodeRegisterCredentialInstruction({
112
+ walletAccountIdx: accountCtx.walletAccountIdx,
113
+ lookupAccountIdx: accountCtx.getAccountIndex(lookupAddressBytes),
114
+ seed: credSeed,
115
+ stateProof,
116
+ });
117
+
118
+ const transaction = await opts.client.transactions.build({
119
+ feePayer: { publicKey: opts.adminPublicKey },
120
+ program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
121
+ instructionData: registerIx,
122
+ accounts: {
123
+ readWrite: [walletAddress, credentialLookupAddress],
124
+ readOnly: [],
125
+ },
126
+ header: { fee: 0n },
127
+ });
128
+
129
+ await transaction.sign(opts.adminPrivateKey);
130
+ const signature = await opts.client.transactions.send(transaction.toWire());
131
+ const result = await trackTransaction(opts.client, signature, 60000);
132
+ if (result.status !== 'finalized') {
133
+ throw new Error(
134
+ `Credential registration failed with status: ${result.status}${
135
+ result.errorCode !== undefined ? ` (error code: ${result.errorCode})` : ''
136
+ }`
137
+ );
138
+ }
139
+ } catch (error) {
140
+ console.warn('Credential registration failed (non-fatal):', error);
141
+ }
142
+ }
143
+ }
144
+
145
+ return {
146
+ walletAddress,
147
+ credentialLookupAddress,
148
+ };
149
+ }