@thru/passkey 0.2.20 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/add-device.cjs +118 -0
- package/dist/auth/add-device.cjs.map +1 -0
- package/dist/auth/add-device.d.cts +69 -0
- package/dist/auth/add-device.d.ts +69 -0
- package/dist/auth/add-device.js +7 -0
- package/dist/auth/add-device.js.map +1 -0
- package/dist/auth.cjs +100 -6
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +2 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +10 -4
- package/dist/auth.js.map +1 -1
- package/dist/{chunk-75G2FPYW.js → chunk-OULTQZT7.js} +4 -3
- package/dist/chunk-OULTQZT7.js.map +1 -0
- package/dist/chunk-QAQNRQ7G.js +104 -0
- package/dist/chunk-QAQNRQ7G.js.map +1 -0
- package/dist/{chunk-B5SN7AS7.js → chunk-TW7HANJM.js} +99 -49
- package/dist/chunk-TW7HANJM.js.map +1 -0
- package/dist/{chunk-2JHC7OOH.js → chunk-ZNBMADOM.js} +2 -2
- package/dist/chunk-ZNBMADOM.js.map +1 -0
- package/dist/index.cjs +102 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -2
- package/dist/mobile.cjs +2 -2
- package/dist/mobile.cjs.map +1 -1
- package/dist/mobile.d.cts +2 -2
- package/dist/mobile.d.ts +2 -2
- package/dist/mobile.js +2 -2
- package/dist/mobile.js.map +1 -1
- package/dist/popup.cjs +3 -2
- package/dist/popup.cjs.map +1 -1
- package/dist/popup.d.cts +3 -3
- package/dist/popup.d.ts +3 -3
- package/dist/popup.js +1 -1
- package/dist/server.cjs +30 -60
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +32 -62
- package/dist/server.js.map +1 -1
- package/dist/{types-_HRzmn-j.d.cts → types-BTTlCVrw.d.cts} +25 -2
- package/dist/{types-_HRzmn-j.d.ts → types-BTTlCVrw.d.ts} +25 -2
- package/dist/web.cjs +99 -48
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.cts +14 -7
- package/dist/web.d.ts +14 -7
- package/dist/web.js +3 -1
- package/package.json +11 -6
- package/src/auth/add-device.ts +213 -0
- package/src/auth/execute-tx.ts +1 -1
- package/src/auth/index.ts +11 -0
- package/src/auth/use-passkey-auth.ts +4 -2
- package/src/capabilities.ts +2 -1
- package/src/index.ts +4 -0
- package/src/label.test.ts +21 -0
- package/src/label.ts +14 -0
- package/src/mobile/index.ts +1 -1
- package/src/mobile/passkey.ts +1 -1
- package/src/mobile/storage.ts +1 -1
- package/src/mobile/types.ts +2 -2
- package/src/popup-service.ts +2 -1
- package/src/register.ts +23 -8
- package/src/server/challenge.ts +2 -2
- package/src/server/create-wallet.test.ts +2 -2
- package/src/server/create-wallet.ts +24 -16
- package/src/server/submit.test.ts +2 -2
- package/src/server/submit.ts +25 -4
- package/src/server/types.ts +2 -2
- package/src/server/utils.ts +1 -1
- package/src/sign.ts +127 -37
- package/src/types.ts +27 -2
- package/src/web.ts +6 -2
- package/tsconfig.json +3 -2
- package/tsup.config.ts +1 -0
- package/dist/chunk-2JHC7OOH.js.map +0 -1
- package/dist/chunk-75G2FPYW.js.map +0 -1
- package/dist/chunk-B5SN7AS7.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thru/passkey",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"import": "./dist/auth.js",
|
|
32
32
|
"require": "./dist/auth.cjs"
|
|
33
33
|
},
|
|
34
|
+
"./auth/add-device": {
|
|
35
|
+
"types": "./dist/auth/add-device.d.ts",
|
|
36
|
+
"import": "./dist/auth/add-device.js",
|
|
37
|
+
"require": "./dist/auth/add-device.cjs"
|
|
38
|
+
},
|
|
34
39
|
"./server": {
|
|
35
40
|
"types": "./dist/server.d.ts",
|
|
36
41
|
"import": "./dist/server.js",
|
|
@@ -38,8 +43,8 @@
|
|
|
38
43
|
}
|
|
39
44
|
},
|
|
40
45
|
"dependencies": {
|
|
41
|
-
"@thru/
|
|
42
|
-
"@thru/
|
|
46
|
+
"@thru/programs": "0.2.22",
|
|
47
|
+
"@thru/sdk": "0.2.22"
|
|
43
48
|
},
|
|
44
49
|
"peerDependencies": {
|
|
45
50
|
"expo-secure-store": "*",
|
|
@@ -62,9 +67,9 @@
|
|
|
62
67
|
}
|
|
63
68
|
},
|
|
64
69
|
"devDependencies": {
|
|
65
|
-
"tsup": "^8.5.
|
|
66
|
-
"typescript": "^
|
|
67
|
-
"vitest": "^
|
|
70
|
+
"tsup": "^8.5.1",
|
|
71
|
+
"typescript": "^6.0.3",
|
|
72
|
+
"vitest": "^4.1.7"
|
|
68
73
|
},
|
|
69
74
|
"scripts": {
|
|
70
75
|
"build": "tsup",
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/* Add-passkey-to-account transaction builder, lifted from
|
|
2
|
+
`web/wallet-auth-manager/app/page.tsx` (`runValidateThen`,
|
|
3
|
+
`handleSubmitAddPasskey`) so the wallet's `/embedded` post-connect
|
|
4
|
+
step can reuse the exact same flow.
|
|
5
|
+
|
|
6
|
+
Builds the on-chain transaction:
|
|
7
|
+
VALIDATE(existingAuthority) + ADD_AUTHORITY(newPasskey) [+ REGISTER_CREDENTIAL]
|
|
8
|
+
asks the caller's existing passkey to sign the challenge, then asks
|
|
9
|
+
the caller's wallet signer to sign the assembled transaction, sends
|
|
10
|
+
it, and returns the result. */
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type Authority,
|
|
14
|
+
buildAccountContext,
|
|
15
|
+
concatenateInstructions,
|
|
16
|
+
createValidateChallenge,
|
|
17
|
+
createCredentialLookupSeed,
|
|
18
|
+
deriveWalletAddress,
|
|
19
|
+
encodeAddAuthorityInstruction,
|
|
20
|
+
encodeRegisterCredentialInstruction,
|
|
21
|
+
encodeValidateInstruction,
|
|
22
|
+
parseWalletAuthorities,
|
|
23
|
+
type ParsedAuthority,
|
|
24
|
+
type WalletSigner,
|
|
25
|
+
} from "@thru/programs/passkey-manager";
|
|
26
|
+
|
|
27
|
+
/** Minimal shape required from a passkey signer. Both web's
|
|
28
|
+
`signWithDiscoverablePasskey`/`signWithPasskey` and mobile's
|
|
29
|
+
counterparts conform. */
|
|
30
|
+
export interface PasskeyChallengeSigner {
|
|
31
|
+
signChallenge: (challenge: Uint8Array) => Promise<{
|
|
32
|
+
signatureR: Uint8Array;
|
|
33
|
+
signatureS: Uint8Array;
|
|
34
|
+
authenticatorData: Uint8Array;
|
|
35
|
+
clientDataJSON: Uint8Array;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Minimal shape required from a Thru chain client. Loosely-typed
|
|
40
|
+
because @thru/sdk's DTS emit is currently broken in this
|
|
41
|
+
repo. The caller passes the real Thru and we narrow operationally. */
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
export type AnyThruClient = any;
|
|
44
|
+
|
|
45
|
+
export interface AddDeviceParams {
|
|
46
|
+
/** Loosely-typed Thru chain client (`@thru/sdk/client`). */
|
|
47
|
+
thru: AnyThruClient;
|
|
48
|
+
/** Wallet (the on-chain WalletAccount) to attach the passkey to. */
|
|
49
|
+
walletAddress: string;
|
|
50
|
+
/** Index of the existing authority that approves this change. Must
|
|
51
|
+
currently be a passkey authority. */
|
|
52
|
+
authIdx: number;
|
|
53
|
+
/** New passkey to attach. tag = 1 (passkey). */
|
|
54
|
+
newAuthority: Authority;
|
|
55
|
+
/** Optional credential-lookup registration so the new passkey is
|
|
56
|
+
discoverable on subsequent sign-ins. */
|
|
57
|
+
credentialId?: Uint8Array;
|
|
58
|
+
walletName?: string;
|
|
59
|
+
/** Existing-passkey challenge signer (web or mobile). */
|
|
60
|
+
passkey: PasskeyChallengeSigner;
|
|
61
|
+
/** Wallet transaction signer that returns base64(signed bytes). */
|
|
62
|
+
walletSigner: WalletSigner;
|
|
63
|
+
/** Passkey program address (base58). */
|
|
64
|
+
programAddress: string;
|
|
65
|
+
/** Sign-and-send executor (lifted from passkey-transaction.ts in the
|
|
66
|
+
wallet-auth-manager - wallet apps own this because it depends on
|
|
67
|
+
the `Thru` client's transaction builder). */
|
|
68
|
+
executor: TxExecutor;
|
|
69
|
+
/** Optional status callback so UIs can show progress. */
|
|
70
|
+
onStatus?: (message: string) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TxExecutorParams {
|
|
74
|
+
thru: AnyThruClient;
|
|
75
|
+
walletSigner: WalletSigner;
|
|
76
|
+
instructionData: Uint8Array;
|
|
77
|
+
readWriteAddresses: string[];
|
|
78
|
+
readOnlyAddresses: string[];
|
|
79
|
+
label: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface TxExecutorResult {
|
|
83
|
+
signature: string;
|
|
84
|
+
/** Loosely-typed because @thru/sdk types aren't available. */
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
execution: any;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type TxExecutor = (
|
|
90
|
+
params: TxExecutorParams,
|
|
91
|
+
) => Promise<TxExecutorResult>;
|
|
92
|
+
|
|
93
|
+
export interface AddDeviceResult extends TxExecutorResult {
|
|
94
|
+
/** The new passkey's authority index after the transaction lands. */
|
|
95
|
+
newAuthorityIdx: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Run VALIDATE + ADD_AUTHORITY [+ REGISTER_CREDENTIAL] to attach a new
|
|
100
|
+
* passkey to an on-chain WalletAccount.
|
|
101
|
+
*/
|
|
102
|
+
export async function addDeviceToAccount(
|
|
103
|
+
params: AddDeviceParams,
|
|
104
|
+
): Promise<AddDeviceResult> {
|
|
105
|
+
const status = params.onStatus ?? (() => {});
|
|
106
|
+
|
|
107
|
+
const walletAccount = await params.thru.accounts.get(params.walletAddress);
|
|
108
|
+
const walletData: Uint8Array | undefined = walletAccount?.data?.data;
|
|
109
|
+
if (!walletData) {
|
|
110
|
+
throw new Error("Wallet account data missing");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parsed = parseWalletAuthorities(walletData);
|
|
114
|
+
const authorizing: ParsedAuthority | undefined =
|
|
115
|
+
parsed.authorities[params.authIdx];
|
|
116
|
+
if (!authorizing) {
|
|
117
|
+
throw new Error("Authorization index out of bounds");
|
|
118
|
+
}
|
|
119
|
+
if (authorizing.kind !== "passkey") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"addDeviceToAccount currently requires a passkey authority for VALIDATE",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* The new authority will land at the next free slot. */
|
|
126
|
+
const newAuthorityIdx = parsed.authorities.length;
|
|
127
|
+
|
|
128
|
+
/* Build the instruction sequence after the VALIDATE. */
|
|
129
|
+
const addAuthorityInstruction = encodeAddAuthorityInstruction({
|
|
130
|
+
authority: params.newAuthority,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let trailingInstructionData = addAuthorityInstruction;
|
|
134
|
+
let readWriteAccounts: Uint8Array[] = [];
|
|
135
|
+
|
|
136
|
+
if (params.credentialId) {
|
|
137
|
+
const lookupSeed = await createCredentialLookupSeed(params.credentialId);
|
|
138
|
+
const lookupAddressBytes = await deriveWalletAddress(
|
|
139
|
+
lookupSeed,
|
|
140
|
+
params.programAddress,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
status("Fetching state proof for credential lookup...");
|
|
144
|
+
const proofResult = await params.thru.proofs.generate({
|
|
145
|
+
proofType: 1 /* StateProofType.CREATING */,
|
|
146
|
+
address: lookupAddressBytes,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const ctx = buildAccountContext({
|
|
150
|
+
walletAddress: params.walletAddress,
|
|
151
|
+
readWriteAccounts: [lookupAddressBytes],
|
|
152
|
+
readOnlyAccounts: [],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const registerCredentialInstruction = encodeRegisterCredentialInstruction({
|
|
156
|
+
walletAccountIdx: ctx.walletAccountIdx,
|
|
157
|
+
lookupAccountIdx: ctx.getAccountIndex(lookupAddressBytes),
|
|
158
|
+
seed: lookupSeed,
|
|
159
|
+
stateProof: proofResult.proof,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
trailingInstructionData = concatenateInstructions([
|
|
163
|
+
addAuthorityInstruction,
|
|
164
|
+
registerCredentialInstruction,
|
|
165
|
+
]);
|
|
166
|
+
readWriteAccounts = [lookupAddressBytes];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Build the VALIDATE challenge over (nonce, account_addresses,
|
|
170
|
+
trailing_instruction_data) and ask the caller's passkey to sign. */
|
|
171
|
+
const ctx = buildAccountContext({
|
|
172
|
+
walletAddress: params.walletAddress,
|
|
173
|
+
readWriteAccounts,
|
|
174
|
+
readOnlyAccounts: [],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const challenge = await createValidateChallenge(
|
|
178
|
+
parsed.nonce,
|
|
179
|
+
ctx.accountAddresses,
|
|
180
|
+
trailingInstructionData,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
status("Waiting for passkey approval...");
|
|
184
|
+
const signature = await params.passkey.signChallenge(challenge);
|
|
185
|
+
|
|
186
|
+
const validateInstruction = encodeValidateInstruction({
|
|
187
|
+
walletAccountIdx: ctx.walletAccountIdx,
|
|
188
|
+
authIdx: params.authIdx,
|
|
189
|
+
signatureR: signature.signatureR,
|
|
190
|
+
signatureS: signature.signatureS,
|
|
191
|
+
authenticatorData: signature.authenticatorData,
|
|
192
|
+
clientDataJSON: signature.clientDataJSON,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const instructionData = concatenateInstructions([
|
|
196
|
+
validateInstruction,
|
|
197
|
+
trailingInstructionData,
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
status("Sending transaction...");
|
|
201
|
+
const result = await params.executor({
|
|
202
|
+
thru: params.thru,
|
|
203
|
+
walletSigner: params.walletSigner,
|
|
204
|
+
instructionData,
|
|
205
|
+
readWriteAddresses: ctx.readWriteAddresses,
|
|
206
|
+
readOnlyAddresses: ctx.readOnlyAddresses,
|
|
207
|
+
label: params.credentialId
|
|
208
|
+
? "VALIDATE + ADD_AUTHORITY + REGISTER_CREDENTIAL"
|
|
209
|
+
: "VALIDATE + ADD_AUTHORITY",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
return { ...result, newAuthorityIdx };
|
|
213
|
+
}
|
package/src/auth/execute-tx.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { base64UrlToBytes, bytesToBase64, bytesToHex } from '@thru/passkey-manager';
|
|
1
|
+
import { base64UrlToBytes, bytesToBase64, bytesToHex } from '@thru/programs/passkey-manager';
|
|
2
2
|
import { signWithPasskey } from '../mobile/passkey';
|
|
3
3
|
import { touchPasskeyLastUsedAt } from '../mobile/storage';
|
|
4
4
|
|
package/src/auth/index.ts
CHANGED
|
@@ -11,6 +11,17 @@ export type {
|
|
|
11
11
|
|
|
12
12
|
export { executePasskeyTransaction } from './execute-tx';
|
|
13
13
|
|
|
14
|
+
export { addDeviceToAccount } from './add-device';
|
|
15
|
+
export type {
|
|
16
|
+
AddDeviceParams,
|
|
17
|
+
AddDeviceResult,
|
|
18
|
+
AnyThruClient,
|
|
19
|
+
PasskeyChallengeSigner,
|
|
20
|
+
TxExecutor,
|
|
21
|
+
TxExecutorParams,
|
|
22
|
+
TxExecutorResult,
|
|
23
|
+
} from './add-device';
|
|
24
|
+
|
|
14
25
|
export {
|
|
15
26
|
createPasskeyAuthStore,
|
|
16
27
|
getPasskeyAuthStore,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bytesToHex } from '@thru/passkey-manager';
|
|
1
|
+
import { bytesToHex } from '@thru/programs/passkey-manager';
|
|
2
2
|
import { create } from 'zustand';
|
|
3
3
|
import { classifyPasskeyError } from '../mobile/errors';
|
|
4
4
|
import {
|
|
@@ -152,8 +152,9 @@ export function createPasskeyAuthStore<TExtra = Record<string, never>>(
|
|
|
152
152
|
|
|
153
153
|
try {
|
|
154
154
|
const tempId = `user-${Date.now()}`;
|
|
155
|
+
const passkeyLabel = resolvedAlias.trim() || 'Thru Wallet';
|
|
155
156
|
const { credentialId, publicKeyX, publicKeyY, rpId } =
|
|
156
|
-
await registerPasskey(
|
|
157
|
+
await registerPasskey(passkeyLabel, tempId, {
|
|
157
158
|
rpId: config.rpId,
|
|
158
159
|
rpName: config.rpName,
|
|
159
160
|
});
|
|
@@ -167,6 +168,7 @@ export function createPasskeyAuthStore<TExtra = Record<string, never>>(
|
|
|
167
168
|
publicKeyX: pubkeyXHex,
|
|
168
169
|
publicKeyY: pubkeyYHex,
|
|
169
170
|
rpId,
|
|
171
|
+
label: passkeyLabel,
|
|
170
172
|
createdAt: now,
|
|
171
173
|
lastUsedAt: now,
|
|
172
174
|
});
|
package/src/capabilities.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PasskeyClientCapabilities } from './types';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const globalProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process;
|
|
4
|
+
const DEBUG = globalProcess?.env?.NEXT_PUBLIC_PASSKEY_DEBUG === '1';
|
|
4
5
|
|
|
5
6
|
let cachedClientCapabilities: PasskeyClientCapabilities | null | undefined;
|
|
6
7
|
let clientCapabilitiesPromise: Promise<PasskeyClientCapabilities | null> | null = null;
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type {
|
|
|
12
12
|
PasskeyClientCapabilities,
|
|
13
13
|
PasskeyPopupContext,
|
|
14
14
|
PasskeyPopupAccount,
|
|
15
|
+
PasskeyStoredSigningOptions,
|
|
16
|
+
PasskeyRegistrationOptions,
|
|
15
17
|
} from './web';
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -19,6 +21,7 @@ export type {
|
|
|
19
21
|
*/
|
|
20
22
|
export {
|
|
21
23
|
registerPasskey,
|
|
24
|
+
createDistinctPasskeyLabel,
|
|
22
25
|
signWithPasskey,
|
|
23
26
|
signWithStoredPasskey,
|
|
24
27
|
signWithDiscoverablePasskey,
|
|
@@ -45,6 +48,7 @@ export {
|
|
|
45
48
|
bytesEqual,
|
|
46
49
|
compareBytes,
|
|
47
50
|
uniqueAccounts,
|
|
51
|
+
type DistinctPasskeyLabelOptions,
|
|
48
52
|
type PasskeyPromptAction,
|
|
49
53
|
} from './web';
|
|
50
54
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createDistinctPasskeyLabel } from './label';
|
|
3
|
+
|
|
4
|
+
describe('createDistinctPasskeyLabel', () => {
|
|
5
|
+
it('keeps a provided passkey label exactly without appending random hex', () => {
|
|
6
|
+
expect(
|
|
7
|
+
createDistinctPasskeyLabel('Jerry iPhone', {
|
|
8
|
+
suffixFactory: () => 'a1b2c3',
|
|
9
|
+
})
|
|
10
|
+
).toBe('Jerry iPhone');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('uses the default fallback exactly when the label is blank', () => {
|
|
14
|
+
expect(
|
|
15
|
+
createDistinctPasskeyLabel(' ', {
|
|
16
|
+
existingLabels: ['Thru Wallet passkey'],
|
|
17
|
+
suffixFactory: () => 'a1b2c3',
|
|
18
|
+
})
|
|
19
|
+
).toBe('Thru Wallet passkey');
|
|
20
|
+
});
|
|
21
|
+
});
|
package/src/label.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const DEFAULT_LABEL = 'Thru Wallet passkey';
|
|
2
|
+
|
|
3
|
+
export interface DistinctPasskeyLabelOptions {
|
|
4
|
+
existingLabels?: Iterable<string | null | undefined>;
|
|
5
|
+
maxAttempts?: number;
|
|
6
|
+
suffixFactory?: () => string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createDistinctPasskeyLabel(
|
|
10
|
+
baseLabel: string,
|
|
11
|
+
_options: DistinctPasskeyLabelOptions = {}
|
|
12
|
+
): string {
|
|
13
|
+
return baseLabel.trim() || DEFAULT_LABEL;
|
|
14
|
+
}
|
package/src/mobile/index.ts
CHANGED
package/src/mobile/passkey.ts
CHANGED
package/src/mobile/storage.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as SecureStore from 'expo-secure-store';
|
|
2
|
-
import type { PasskeyMetadata } from '@thru/passkey-manager';
|
|
2
|
+
import type { PasskeyMetadata } from '@thru/programs/passkey-manager';
|
|
3
3
|
|
|
4
4
|
const SECURE_STORE_OPTS = {
|
|
5
5
|
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
|
package/src/mobile/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { PasskeySigningResult, PasskeyMetadata } from '@thru/passkey-manager';
|
|
1
|
+
import type { PasskeySigningResult, PasskeyMetadata } from '@thru/programs/passkey-manager';
|
|
2
2
|
|
|
3
|
-
export type { PasskeySigningResult, PasskeyMetadata } from '@thru/passkey-manager';
|
|
3
|
+
export type { PasskeySigningResult, PasskeyMetadata } from '@thru/programs/passkey-manager';
|
|
4
4
|
|
|
5
5
|
export interface PasskeyMobileConfig {
|
|
6
6
|
rpId?: string;
|
package/src/popup-service.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
import {
|
|
8
8
|
PASSKEY_POPUP_RESPONSE_EVENT,
|
|
9
9
|
} from './popup';
|
|
10
|
-
import { bytesToBase64Url, base64UrlToBytes } from '@thru/passkey-manager';
|
|
10
|
+
import { bytesToBase64Url, base64UrlToBytes } from '@thru/programs/passkey-manager';
|
|
11
11
|
export function toPopupSigningResult(result: PasskeySigningResult): PasskeyPopupSigningResult {
|
|
12
12
|
return {
|
|
13
13
|
signatureBase64Url: bytesToBase64Url(result.signature),
|
|
@@ -15,6 +15,7 @@ export function toPopupSigningResult(result: PasskeySigningResult): PasskeyPopup
|
|
|
15
15
|
clientDataJSONBase64Url: bytesToBase64Url(result.clientDataJSON),
|
|
16
16
|
signatureRBase64Url: bytesToBase64Url(result.signatureR),
|
|
17
17
|
signatureSBase64Url: bytesToBase64Url(result.signatureS),
|
|
18
|
+
authenticatorAttachment: result.authenticatorAttachment ?? null,
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
|
package/src/register.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
PasskeyRegistrationOptions,
|
|
3
|
+
PasskeyRegistrationResult,
|
|
4
|
+
PasskeyPopupRegistrationResult,
|
|
5
|
+
} from './types';
|
|
6
|
+
import { arrayBufferToBase64Url, bytesToHex } from '@thru/programs/passkey-manager';
|
|
3
7
|
import {
|
|
4
8
|
isWebAuthnSupported,
|
|
5
9
|
getPasskeyPromptMode,
|
|
@@ -15,7 +19,8 @@ import { requestPasskeyPopup, openPasskeyPopupWindow, closePopup } from './popup
|
|
|
15
19
|
export async function registerPasskey(
|
|
16
20
|
alias: string,
|
|
17
21
|
userId: string,
|
|
18
|
-
rpId: string
|
|
22
|
+
rpId: string,
|
|
23
|
+
options: PasskeyRegistrationOptions = {}
|
|
19
24
|
): Promise<PasskeyRegistrationResult> {
|
|
20
25
|
if (!isWebAuthnSupported()) {
|
|
21
26
|
throw new Error('WebAuthn is not supported in this browser');
|
|
@@ -24,17 +29,20 @@ export async function registerPasskey(
|
|
|
24
29
|
return runWithPromptMode(
|
|
25
30
|
'create',
|
|
26
31
|
() => registerPasskeyInline(alias, userId, rpId),
|
|
27
|
-
(preopenedPopup) => registerPasskeyViaPopup(alias, userId, rpId, preopenedPopup)
|
|
32
|
+
(preopenedPopup) => registerPasskeyViaPopup(alias, userId, rpId, preopenedPopup),
|
|
33
|
+
options
|
|
28
34
|
);
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
async function runWithPromptMode<T>(
|
|
32
38
|
action: PasskeyPromptAction,
|
|
33
39
|
inlineFn: () => Promise<T>,
|
|
34
|
-
popupFn: (preopenedPopup?: Window | null) => Promise<T
|
|
40
|
+
popupFn: (preopenedPopup?: Window | null) => Promise<T>,
|
|
41
|
+
options: PasskeyRegistrationOptions = {}
|
|
35
42
|
): Promise<T> {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
43
|
+
const allowPopupFallback = options.allowPopupFallback ?? true;
|
|
44
|
+
const preopenedPopup = allowPopupFallback ? maybePreopenPopup(action, openPasskeyPopupWindow) : null;
|
|
45
|
+
const promptMode = allowPopupFallback ? await getPasskeyPromptMode(action) : 'inline';
|
|
38
46
|
if (promptMode === 'popup') {
|
|
39
47
|
return popupFn(preopenedPopup);
|
|
40
48
|
}
|
|
@@ -44,7 +52,7 @@ async function runWithPromptMode<T>(
|
|
|
44
52
|
try {
|
|
45
53
|
return await inlineFn();
|
|
46
54
|
} catch (error) {
|
|
47
|
-
if (shouldFallbackToPopup(error)) {
|
|
55
|
+
if (allowPopupFallback && shouldFallbackToPopup(error)) {
|
|
48
56
|
return popupFn();
|
|
49
57
|
}
|
|
50
58
|
throw error;
|
|
@@ -97,12 +105,19 @@ async function registerPasskeyInline(
|
|
|
97
105
|
|
|
98
106
|
const response = credential.response as AuthenticatorAttestationResponse;
|
|
99
107
|
const { x, y } = extractP256PublicKey(response);
|
|
108
|
+
const authenticatorAttachment =
|
|
109
|
+
(
|
|
110
|
+
credential as PublicKeyCredential & {
|
|
111
|
+
authenticatorAttachment?: AuthenticatorAttachment | null;
|
|
112
|
+
}
|
|
113
|
+
).authenticatorAttachment ?? null;
|
|
100
114
|
|
|
101
115
|
return {
|
|
102
116
|
credentialId: arrayBufferToBase64Url(credential.rawId),
|
|
103
117
|
publicKeyX: bytesToHex(x),
|
|
104
118
|
publicKeyY: bytesToHex(y),
|
|
105
119
|
rpId,
|
|
120
|
+
authenticatorAttachment,
|
|
106
121
|
};
|
|
107
122
|
}
|
|
108
123
|
|
package/src/server/challenge.ts
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
bytesToBase64Url,
|
|
3
3
|
createValidateChallenge,
|
|
4
4
|
fetchWalletNonce,
|
|
5
|
-
} from '@thru/passkey-manager';
|
|
6
|
-
import type { AccountContext } from '@thru/passkey-manager';
|
|
5
|
+
} from '@thru/programs/passkey-manager';
|
|
6
|
+
import type { AccountContext } from '@thru/programs/passkey-manager';
|
|
7
7
|
import type { PasskeyChallengeResult, ThruClient } from './types';
|
|
8
8
|
|
|
9
9
|
export async function createPasskeyChallenge(opts: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { ThruClient } from './types';
|
|
3
3
|
|
|
4
|
-
vi.mock('@thru/helpers', () => ({
|
|
4
|
+
vi.mock('@thru/sdk/helpers', () => ({
|
|
5
5
|
encodeAddress: (bytes: Uint8Array) => {
|
|
6
6
|
const first = bytes[0];
|
|
7
7
|
if (first === 11) return 'wallet-address';
|
|
@@ -10,7 +10,7 @@ vi.mock('@thru/helpers', () => ({
|
|
|
10
10
|
},
|
|
11
11
|
}));
|
|
12
12
|
|
|
13
|
-
vi.mock('@thru/passkey-manager', () => ({
|
|
13
|
+
vi.mock('@thru/programs/passkey-manager', () => ({
|
|
14
14
|
PASSKEY_MANAGER_PROGRAM_ADDRESS: 'passkey-program',
|
|
15
15
|
base64UrlToBytes: () => new Uint8Array([7]),
|
|
16
16
|
buildAccountContext: (params: { readWriteAccounts: Uint8Array[] }) => ({
|
|
@@ -8,14 +8,14 @@ import {
|
|
|
8
8
|
deriveWalletAddress,
|
|
9
9
|
encodeCreateInstruction,
|
|
10
10
|
encodeRegisterCredentialInstruction,
|
|
11
|
-
} from '@thru/passkey-manager';
|
|
11
|
+
} from '@thru/programs/passkey-manager';
|
|
12
12
|
import {
|
|
13
13
|
toThruAddress,
|
|
14
14
|
getStateProof,
|
|
15
15
|
trackTransaction,
|
|
16
16
|
withSerializedFeePayer,
|
|
17
|
-
} from
|
|
18
|
-
import type { ThruClient } from
|
|
17
|
+
} from "./utils";
|
|
18
|
+
import type { ThruClient } from "./types";
|
|
19
19
|
|
|
20
20
|
export async function createPasskeyWallet(opts: {
|
|
21
21
|
client: ThruClient;
|
|
@@ -27,9 +27,12 @@ export async function createPasskeyWallet(opts: {
|
|
|
27
27
|
credentialId?: string;
|
|
28
28
|
walletName?: string;
|
|
29
29
|
}): Promise<{ walletAddress: string; credentialLookupAddress?: string }> {
|
|
30
|
-
const walletName = opts.walletName ??
|
|
30
|
+
const walletName = opts.walletName ?? "default";
|
|
31
31
|
const seed = await createWalletSeed(walletName, opts.pubkeyX, opts.pubkeyY);
|
|
32
|
-
const walletBytes = await deriveWalletAddress(
|
|
32
|
+
const walletBytes = await deriveWalletAddress(
|
|
33
|
+
seed,
|
|
34
|
+
PASSKEY_MANAGER_PROGRAM_ADDRESS,
|
|
35
|
+
);
|
|
33
36
|
const walletAddress = toThruAddress(walletBytes);
|
|
34
37
|
|
|
35
38
|
await withSerializedFeePayer(opts.adminPublicKey, async () => {
|
|
@@ -77,11 +80,13 @@ export async function createPasskeyWallet(opts: {
|
|
|
77
80
|
await transaction.sign(opts.adminPrivateKey);
|
|
78
81
|
const signature = await opts.client.transactions.send(transaction.toWire());
|
|
79
82
|
const result = await trackTransaction(opts.client, signature, 60000);
|
|
80
|
-
if (result.status !==
|
|
83
|
+
if (result.status !== "finalized") {
|
|
81
84
|
throw new Error(
|
|
82
85
|
`Wallet creation failed with status: ${result.status}${
|
|
83
|
-
result.errorCode !== undefined
|
|
84
|
-
|
|
86
|
+
result.errorCode !== undefined
|
|
87
|
+
? ` (error code: ${result.errorCode})`
|
|
88
|
+
: ""
|
|
89
|
+
}`,
|
|
85
90
|
);
|
|
86
91
|
}
|
|
87
92
|
});
|
|
@@ -91,8 +96,7 @@ export async function createPasskeyWallet(opts: {
|
|
|
91
96
|
const credentialIdBytes = base64UrlToBytes(opts.credentialId);
|
|
92
97
|
const lookupAddressBytes = await deriveCredentialLookupAddress(
|
|
93
98
|
credentialIdBytes,
|
|
94
|
-
|
|
95
|
-
PASSKEY_MANAGER_PROGRAM_ADDRESS
|
|
99
|
+
PASSKEY_MANAGER_PROGRAM_ADDRESS,
|
|
96
100
|
);
|
|
97
101
|
const lookupAddress = toThruAddress(lookupAddressBytes);
|
|
98
102
|
|
|
@@ -110,7 +114,7 @@ export async function createPasskeyWallet(opts: {
|
|
|
110
114
|
|
|
111
115
|
if (lookupExists) return;
|
|
112
116
|
|
|
113
|
-
const credSeed = await createCredentialLookupSeed(credentialIdBytes
|
|
117
|
+
const credSeed = await createCredentialLookupSeed(credentialIdBytes);
|
|
114
118
|
const stateProof = await getStateProof(opts.client, lookupAddress);
|
|
115
119
|
const accountCtx = buildAccountContext({
|
|
116
120
|
walletAddress,
|
|
@@ -139,18 +143,22 @@ export async function createPasskeyWallet(opts: {
|
|
|
139
143
|
});
|
|
140
144
|
|
|
141
145
|
await transaction.sign(opts.adminPrivateKey);
|
|
142
|
-
const signature = await opts.client.transactions.send(
|
|
146
|
+
const signature = await opts.client.transactions.send(
|
|
147
|
+
transaction.toWire(),
|
|
148
|
+
);
|
|
143
149
|
const result = await trackTransaction(opts.client, signature, 60000);
|
|
144
|
-
if (result.status !==
|
|
150
|
+
if (result.status !== "finalized") {
|
|
145
151
|
throw new Error(
|
|
146
152
|
`Credential registration failed with status: ${result.status}${
|
|
147
|
-
result.errorCode !== undefined
|
|
148
|
-
|
|
153
|
+
result.errorCode !== undefined
|
|
154
|
+
? ` (error code: ${result.errorCode})`
|
|
155
|
+
: ""
|
|
156
|
+
}`,
|
|
149
157
|
);
|
|
150
158
|
}
|
|
151
159
|
});
|
|
152
160
|
} catch (error) {
|
|
153
|
-
console.warn(
|
|
161
|
+
console.warn("Credential registration failed (non-fatal):", error);
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import type { AccountContext } from '@thru/passkey-manager';
|
|
2
|
+
import type { AccountContext } from '@thru/programs/passkey-manager';
|
|
3
3
|
import type { ThruClient } from './types';
|
|
4
4
|
|
|
5
|
-
vi.mock('@thru/passkey-manager', () => ({
|
|
5
|
+
vi.mock('@thru/programs/passkey-manager', () => ({
|
|
6
6
|
PASSKEY_MANAGER_PROGRAM_ADDRESS: 'passkey-program',
|
|
7
7
|
concatenateInstructions: (instructions: Uint8Array[]) => new Uint8Array(instructions.flatMap((ix) => Array.from(ix))),
|
|
8
8
|
encodeValidateInstruction: () => new Uint8Array([1, 2]),
|