@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.
- package/README.md +73 -90
- package/dist/auth.cjs +672 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +60 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +422 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-2JHC7OOH.js +250 -0
- package/dist/chunk-2JHC7OOH.js.map +1 -0
- package/dist/chunk-75G2FPYW.js +54 -0
- package/dist/chunk-75G2FPYW.js.map +1 -0
- package/dist/chunk-B5SN7AS7.js +586 -0
- package/dist/chunk-B5SN7AS7.js.map +1 -0
- package/dist/chunk-LNDWK3FA.js +163 -0
- package/dist/chunk-LNDWK3FA.js.map +1 -0
- package/dist/index.cjs +27 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -187
- package/dist/index.d.ts +4 -187
- package/dist/index.js +47 -810
- package/dist/index.js.map +1 -1
- package/dist/mobile.cjs +301 -0
- package/dist/mobile.cjs.map +1 -0
- package/dist/mobile.d.cts +49 -0
- package/dist/mobile.d.ts +49 -0
- package/dist/mobile.js +41 -0
- package/dist/mobile.js.map +1 -0
- package/dist/popup.cjs +247 -0
- package/dist/popup.cjs.map +1 -0
- package/dist/popup.d.cts +22 -0
- package/dist/popup.d.ts +22 -0
- package/dist/popup.js +31 -0
- package/dist/popup.js.map +1 -0
- package/dist/server.cjs +351 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +119 -0
- package/dist/server.d.ts +119 -0
- package/dist/server.js +340 -0
- package/dist/server.js.map +1 -0
- package/dist/types-_HRzmn-j.d.cts +125 -0
- package/dist/types-_HRzmn-j.d.ts +125 -0
- package/dist/web.cjs +758 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.cts +32 -0
- package/dist/web.d.ts +32 -0
- package/dist/web.js +60 -0
- package/dist/web.js.map +1 -0
- package/package.json +47 -2
- package/src/auth/execute-tx.ts +87 -0
- package/src/auth/index.ts +18 -0
- package/src/auth/types.ts +56 -0
- package/src/auth/use-passkey-auth.ts +428 -0
- package/src/index.ts +37 -39
- package/src/mobile/errors.ts +31 -0
- package/src/mobile/index.ts +33 -0
- package/src/mobile/passkey.ts +154 -0
- package/src/mobile/storage.ts +115 -0
- package/src/mobile/types.ts +24 -0
- package/src/popup-entry.ts +33 -0
- package/src/popup-service.ts +0 -103
- package/src/server/challenge.ts +26 -0
- package/src/server/create-wallet.ts +149 -0
- package/src/server/handlers.ts +93 -0
- package/src/server/index.ts +13 -0
- package/src/server/submit.ts +47 -0
- package/src/server/types.ts +70 -0
- package/src/server/utils.ts +69 -0
- package/src/types.ts +1 -0
- package/src/web.ts +51 -0
- package/tsconfig.json +6 -1
- 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';
|
package/src/popup-service.ts
CHANGED
|
@@ -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
|
+
}
|