@thru/passkey 0.2.21 → 0.2.23
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 +139 -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 +121 -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-KASTJBBY.js +128 -0
- package/dist/chunk-KASTJBBY.js.map +1 -0
- package/dist/{chunk-75G2FPYW.js → chunk-OULTQZT7.js} +4 -3
- package/dist/chunk-OULTQZT7.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 +54 -66
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +14 -6
- package/dist/server.d.ts +14 -6
- package/dist/server.js +58 -69
- 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 +236 -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 +15 -4
- package/src/server/create-wallet.test.ts +2 -2
- package/src/server/create-wallet.ts +24 -16
- package/src/server/handlers.ts +6 -2
- package/src/server/submit.test.ts +24 -6
- package/src/server/submit.ts +41 -10
- package/src/server/types.ts +5 -3
- 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
|
@@ -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
|
|
package/src/server/handlers.ts
CHANGED
|
@@ -45,7 +45,9 @@ export function createPasskeyHandlers<P>(opts: {
|
|
|
45
45
|
client: opts.client,
|
|
46
46
|
walletAddress,
|
|
47
47
|
accountCtx: context.accountCtx,
|
|
48
|
-
|
|
48
|
+
targetProgramAddress: context.targetProgramAddress,
|
|
49
|
+
instructionData: context.instructionData,
|
|
50
|
+
authIdx: context.authIdx,
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
pendingContexts.set(
|
|
@@ -85,7 +87,9 @@ export function createPasskeyHandlers<P>(opts: {
|
|
|
85
87
|
adminPrivateKey: opts.adminPrivateKey,
|
|
86
88
|
walletAddress,
|
|
87
89
|
accountCtx: pending.context.accountCtx,
|
|
88
|
-
|
|
90
|
+
targetProgramAddress: pending.context.targetProgramAddress,
|
|
91
|
+
instructionData: pending.context.instructionData,
|
|
92
|
+
authIdx: pending.context.authIdx,
|
|
89
93
|
...signaturePayload,
|
|
90
94
|
});
|
|
91
95
|
},
|
|
@@ -1,20 +1,24 @@
|
|
|
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.
|
|
5
|
+
const passkeyManagerMocks = vi.hoisted(() => ({
|
|
6
6
|
PASSKEY_MANAGER_PROGRAM_ADDRESS: 'passkey-program',
|
|
7
|
-
|
|
8
|
-
encodeValidateInstruction: () => new Uint8Array([1, 2]),
|
|
7
|
+
decodeAddress: vi.fn(() => new Uint8Array(32).fill(7)),
|
|
8
|
+
encodeValidateInstruction: vi.fn(() => new Uint8Array([1, 2])),
|
|
9
9
|
hexToBytes: (value: string) => new Uint8Array(Buffer.from(value, 'hex')),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
+
vi.mock('@thru/programs/passkey-manager', () => passkeyManagerMocks);
|
|
13
|
+
|
|
12
14
|
import { buildPasskeyTransaction, submitPasskeyTransaction } from './submit';
|
|
13
15
|
|
|
14
16
|
const accountCtx = {
|
|
15
17
|
walletAccountIdx: 3,
|
|
18
|
+
accountAddresses: ['fee-payer', 'passkey-program', 'wallet-address', 'readonly-address'],
|
|
16
19
|
readWriteAddresses: ['lookup-address', 'wallet-address'],
|
|
17
20
|
readOnlyAddresses: ['readonly-address'],
|
|
21
|
+
getAccountIndex: vi.fn(() => 4),
|
|
18
22
|
} as AccountContext;
|
|
19
23
|
|
|
20
24
|
const signaturePayload = {
|
|
@@ -58,7 +62,8 @@ describe('passkey submit', () => {
|
|
|
58
62
|
adminPrivateKey: new Uint8Array(32).fill(2),
|
|
59
63
|
walletAddress: 'wallet-address',
|
|
60
64
|
accountCtx,
|
|
61
|
-
|
|
65
|
+
targetProgramAddress: 'target-program',
|
|
66
|
+
instructionData: new Uint8Array([3, 4]),
|
|
62
67
|
header: {
|
|
63
68
|
fee: 0n,
|
|
64
69
|
nonce: 42n,
|
|
@@ -76,7 +81,19 @@ describe('passkey submit', () => {
|
|
|
76
81
|
fee: 0n,
|
|
77
82
|
nonce: 42n,
|
|
78
83
|
},
|
|
84
|
+
instructionData: new Uint8Array([1, 2]),
|
|
79
85
|
}));
|
|
86
|
+
expect(passkeyManagerMocks.decodeAddress).toHaveBeenCalledWith('target-program');
|
|
87
|
+
expect(passkeyManagerMocks.encodeValidateInstruction).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
walletAccountIdx: 3,
|
|
90
|
+
authIdx: 0,
|
|
91
|
+
targetInstruction: {
|
|
92
|
+
programIdx: 4,
|
|
93
|
+
instructionData: new Uint8Array([3, 4]),
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
);
|
|
80
97
|
expect(transaction.sign).toHaveBeenCalledWith(new Uint8Array(32).fill(2));
|
|
81
98
|
expect(result.rawTransaction).toEqual(new Uint8Array([9, 9, 9]));
|
|
82
99
|
});
|
|
@@ -90,7 +107,8 @@ describe('passkey submit', () => {
|
|
|
90
107
|
adminPrivateKey: new Uint8Array(32).fill(2),
|
|
91
108
|
walletAddress: 'wallet-address',
|
|
92
109
|
accountCtx,
|
|
93
|
-
|
|
110
|
+
targetProgramAddress: 'target-program',
|
|
111
|
+
instructionData: new Uint8Array([3, 4]),
|
|
94
112
|
...signaturePayload,
|
|
95
113
|
})).resolves.toEqual({
|
|
96
114
|
signature: 'tx-signature',
|
package/src/server/submit.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PASSKEY_MANAGER_PROGRAM_ADDRESS,
|
|
3
|
-
|
|
3
|
+
decodeAddress,
|
|
4
4
|
encodeValidateInstruction,
|
|
5
5
|
hexToBytes,
|
|
6
|
-
} from '@thru/passkey-manager';
|
|
7
|
-
import type { AccountContext } from '@thru/passkey-manager';
|
|
6
|
+
} from '@thru/programs/passkey-manager';
|
|
7
|
+
import type { AccountContext } from '@thru/programs/passkey-manager';
|
|
8
8
|
import { trackTransaction, withSerializedFeePayer } from './utils';
|
|
9
9
|
import type {
|
|
10
10
|
BuiltPasskeyTransaction,
|
|
@@ -14,6 +14,27 @@ import type {
|
|
|
14
14
|
TransactionResult,
|
|
15
15
|
} from './types';
|
|
16
16
|
|
|
17
|
+
function base64ToBytes(base64: string): Uint8Array {
|
|
18
|
+
type BufferLike = {
|
|
19
|
+
from(value: string, encoding: 'base64'): Uint8Array;
|
|
20
|
+
};
|
|
21
|
+
const globalBuffer = (globalThis as { Buffer?: BufferLike }).Buffer;
|
|
22
|
+
if (globalBuffer) {
|
|
23
|
+
return globalBuffer.from(base64, 'base64');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof atob === 'function') {
|
|
27
|
+
const binary = atob(base64);
|
|
28
|
+
const bytes = new Uint8Array(binary.length);
|
|
29
|
+
for (let i = 0; i < binary.length; i++) {
|
|
30
|
+
bytes[i] = binary.charCodeAt(i);
|
|
31
|
+
}
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error('Base64 decoding is not supported in this environment');
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
/**
|
|
18
39
|
* Builds and signs a passkey-manager transaction without submitting it.
|
|
19
40
|
*
|
|
@@ -27,23 +48,31 @@ export async function buildPasskeyTransaction(opts: {
|
|
|
27
48
|
adminPrivateKey: Uint8Array;
|
|
28
49
|
walletAddress: string;
|
|
29
50
|
accountCtx: AccountContext;
|
|
30
|
-
|
|
51
|
+
targetProgramAddress: string;
|
|
52
|
+
instructionData: Uint8Array;
|
|
53
|
+
authIdx?: number;
|
|
31
54
|
header?: PasskeyTransactionHeaderOverrides;
|
|
32
55
|
} & PasskeySignaturePayload): Promise<BuiltPasskeyTransaction> {
|
|
56
|
+
const targetProgramIdx = opts.accountCtx.getAccountIndex(
|
|
57
|
+
decodeAddress(opts.targetProgramAddress)
|
|
58
|
+
);
|
|
33
59
|
const validateIx = encodeValidateInstruction({
|
|
34
60
|
walletAccountIdx: opts.accountCtx.walletAccountIdx,
|
|
35
|
-
authIdx: 0,
|
|
61
|
+
authIdx: opts.authIdx ?? 0,
|
|
62
|
+
targetInstruction: {
|
|
63
|
+
programIdx: targetProgramIdx,
|
|
64
|
+
instructionData: opts.instructionData,
|
|
65
|
+
},
|
|
36
66
|
signatureR: hexToBytes(opts.signatureR),
|
|
37
67
|
signatureS: hexToBytes(opts.signatureS),
|
|
38
|
-
authenticatorData:
|
|
39
|
-
clientDataJSON:
|
|
68
|
+
authenticatorData: base64ToBytes(opts.authenticatorData),
|
|
69
|
+
clientDataJSON: base64ToBytes(opts.clientDataJSON),
|
|
40
70
|
});
|
|
41
71
|
|
|
42
|
-
const instructionData = concatenateInstructions([validateIx, opts.invokeIx]);
|
|
43
72
|
const transaction = await opts.client.transactions.build({
|
|
44
73
|
feePayer: { publicKey: opts.adminPublicKey },
|
|
45
74
|
program: PASSKEY_MANAGER_PROGRAM_ADDRESS,
|
|
46
|
-
instructionData,
|
|
75
|
+
instructionData: validateIx,
|
|
47
76
|
accounts: {
|
|
48
77
|
readWrite: opts.accountCtx.readWriteAddresses,
|
|
49
78
|
readOnly: opts.accountCtx.readOnlyAddresses,
|
|
@@ -67,7 +96,9 @@ export async function submitPasskeyTransaction(opts: {
|
|
|
67
96
|
adminPrivateKey: Uint8Array;
|
|
68
97
|
walletAddress: string;
|
|
69
98
|
accountCtx: AccountContext;
|
|
70
|
-
|
|
99
|
+
targetProgramAddress: string;
|
|
100
|
+
instructionData: Uint8Array;
|
|
101
|
+
authIdx?: number;
|
|
71
102
|
header?: PasskeyTransactionHeaderOverrides;
|
|
72
103
|
} & PasskeySignaturePayload): Promise<TransactionResult> {
|
|
73
104
|
return withSerializedFeePayer(opts.adminPublicKey, async () => {
|
package/src/server/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { AccountContext } from '@thru/passkey-manager';
|
|
2
|
-
import type { TransactionHeaderConfig } from '@thru/
|
|
1
|
+
import type { AccountContext } from '@thru/programs/passkey-manager';
|
|
2
|
+
import type { TransactionHeaderConfig } from '@thru/sdk';
|
|
3
3
|
|
|
4
4
|
export type PasskeyTransactionHeaderOverrides = TransactionHeaderConfig;
|
|
5
5
|
|
|
@@ -79,5 +79,7 @@ export interface PasskeyChallengeResult {
|
|
|
79
79
|
|
|
80
80
|
export interface PasskeyContextResult {
|
|
81
81
|
accountCtx: AccountContext;
|
|
82
|
-
|
|
82
|
+
targetProgramAddress: string;
|
|
83
|
+
instructionData: Uint8Array;
|
|
84
|
+
authIdx?: number;
|
|
83
85
|
}
|
package/src/server/utils.ts
CHANGED
package/src/sign.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
PasskeyPopupContext,
|
|
7
7
|
PasskeyPopupSigningResult,
|
|
8
8
|
PasskeyPopupStoredSigningResult,
|
|
9
|
+
PasskeyStoredSigningOptions,
|
|
9
10
|
} from './types';
|
|
10
11
|
import {
|
|
11
12
|
arrayBufferToBase64Url,
|
|
@@ -14,7 +15,7 @@ import {
|
|
|
14
15
|
base64UrlToBytes,
|
|
15
16
|
parseDerSignature,
|
|
16
17
|
normalizeLowS,
|
|
17
|
-
} from '@thru/passkey-manager';
|
|
18
|
+
} from '@thru/programs/passkey-manager';
|
|
18
19
|
import {
|
|
19
20
|
isWebAuthnSupported,
|
|
20
21
|
getPasskeyPromptMode,
|
|
@@ -23,7 +24,11 @@ import {
|
|
|
23
24
|
shouldFallbackToPopup,
|
|
24
25
|
type PasskeyPromptAction,
|
|
25
26
|
} from './capabilities';
|
|
26
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
requestPasskeyPopup,
|
|
29
|
+
openPasskeyPopupWindow,
|
|
30
|
+
closePopup,
|
|
31
|
+
} from './popup';
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Sign a challenge with an existing passkey (by credential ID).
|
|
@@ -40,7 +45,8 @@ export async function signWithPasskey(
|
|
|
40
45
|
return runWithPromptMode(
|
|
41
46
|
'get',
|
|
42
47
|
() => signWithPasskeyInline(credentialId, challenge, rpId),
|
|
43
|
-
(preopenedPopup) =>
|
|
48
|
+
(preopenedPopup) =>
|
|
49
|
+
signWithPasskeyViaPopup(credentialId, challenge, rpId, preopenedPopup)
|
|
44
50
|
);
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -52,16 +58,31 @@ export async function signWithStoredPasskey(
|
|
|
52
58
|
rpId: string,
|
|
53
59
|
preferredPasskey: PasskeyMetadata | null,
|
|
54
60
|
allPasskeys: PasskeyMetadata[],
|
|
55
|
-
context?: PasskeyPopupContext
|
|
61
|
+
context?: PasskeyPopupContext,
|
|
62
|
+
options: PasskeyStoredSigningOptions = {}
|
|
56
63
|
): Promise<PasskeyStoredSigningResult> {
|
|
57
64
|
if (!isWebAuthnSupported()) {
|
|
58
65
|
throw new Error('WebAuthn is not supported in this browser');
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
const
|
|
62
|
-
const
|
|
68
|
+
const allowPopupFallback = options.allowPopupFallback ?? true;
|
|
69
|
+
const preopenedPopup = allowPopupFallback
|
|
70
|
+
? maybePreopenPopup('get', openPasskeyPopupWindow)
|
|
71
|
+
: null;
|
|
72
|
+
const promptMode = allowPopupFallback
|
|
73
|
+
? await getPasskeyPromptMode('get')
|
|
74
|
+
: 'inline';
|
|
63
75
|
const storedPasskey = preferredPasskey;
|
|
64
|
-
const canUsePopup = isInIframe();
|
|
76
|
+
const canUsePopup = allowPopupFallback && isInIframe();
|
|
77
|
+
|
|
78
|
+
if (options.preferDiscoverable) {
|
|
79
|
+
closePopup(preopenedPopup);
|
|
80
|
+
return signWithDiscoverableStoredPasskey(
|
|
81
|
+
challenge,
|
|
82
|
+
storedPasskey?.rpId ?? rpId,
|
|
83
|
+
allPasskeys
|
|
84
|
+
);
|
|
85
|
+
}
|
|
65
86
|
|
|
66
87
|
if (promptMode === 'popup' || (canUsePopup && !storedPasskey)) {
|
|
67
88
|
return requestStoredPasskeyPopup(challenge, preopenedPopup, context);
|
|
@@ -71,37 +92,29 @@ export async function signWithStoredPasskey(
|
|
|
71
92
|
|
|
72
93
|
try {
|
|
73
94
|
if (storedPasskey) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
95
|
+
try {
|
|
96
|
+
const result = await signWithPasskeyInline(
|
|
97
|
+
storedPasskey.credentialId,
|
|
98
|
+
challenge,
|
|
99
|
+
storedPasskey.rpId
|
|
100
|
+
);
|
|
101
|
+
return {
|
|
102
|
+
...result,
|
|
103
|
+
passkey: storedPasskey,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!shouldFallbackToDiscoverable(error)) {
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return signWithDiscoverableStoredPasskey(
|
|
110
|
+
challenge,
|
|
111
|
+
storedPasskey.rpId,
|
|
112
|
+
allPasskeys
|
|
113
|
+
);
|
|
114
|
+
}
|
|
83
115
|
}
|
|
84
116
|
|
|
85
|
-
|
|
86
|
-
const matchingPasskey = allPasskeys.find(p => p.credentialId === discoverable.credentialId) ?? null;
|
|
87
|
-
const now = new Date().toISOString();
|
|
88
|
-
const passkey = matchingPasskey ?? {
|
|
89
|
-
credentialId: discoverable.credentialId,
|
|
90
|
-
publicKeyX: '',
|
|
91
|
-
publicKeyY: '',
|
|
92
|
-
rpId: discoverable.rpId,
|
|
93
|
-
createdAt: now,
|
|
94
|
-
lastUsedAt: now,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
signature: discoverable.signature,
|
|
99
|
-
authenticatorData: discoverable.authenticatorData,
|
|
100
|
-
clientDataJSON: discoverable.clientDataJSON,
|
|
101
|
-
signatureR: discoverable.signatureR,
|
|
102
|
-
signatureS: discoverable.signatureS,
|
|
103
|
-
passkey,
|
|
104
|
-
};
|
|
117
|
+
return signWithDiscoverableStoredPasskey(challenge, rpId, allPasskeys);
|
|
105
118
|
} catch (error) {
|
|
106
119
|
if (canUsePopup && shouldFallbackToPopup(error)) {
|
|
107
120
|
return requestStoredPasskeyPopup(challenge, undefined, context);
|
|
@@ -111,6 +124,66 @@ export async function signWithStoredPasskey(
|
|
|
111
124
|
}
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
async function signWithDiscoverableStoredPasskey(
|
|
128
|
+
challenge: Uint8Array,
|
|
129
|
+
rpId: string,
|
|
130
|
+
allPasskeys: PasskeyMetadata[]
|
|
131
|
+
): Promise<PasskeyStoredSigningResult> {
|
|
132
|
+
const discoverable = await signWithDiscoverablePasskey(challenge, rpId);
|
|
133
|
+
const matchingPasskey =
|
|
134
|
+
allPasskeys.find((p) => p.credentialId === discoverable.credentialId) ??
|
|
135
|
+
null;
|
|
136
|
+
const now = new Date().toISOString();
|
|
137
|
+
const passkey = matchingPasskey ?? {
|
|
138
|
+
credentialId: discoverable.credentialId,
|
|
139
|
+
publicKeyX: '',
|
|
140
|
+
publicKeyY: '',
|
|
141
|
+
rpId: discoverable.rpId,
|
|
142
|
+
createdAt: now,
|
|
143
|
+
lastUsedAt: now,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
signature: discoverable.signature,
|
|
148
|
+
authenticatorData: discoverable.authenticatorData,
|
|
149
|
+
clientDataJSON: discoverable.clientDataJSON,
|
|
150
|
+
signatureR: discoverable.signatureR,
|
|
151
|
+
signatureS: discoverable.signatureS,
|
|
152
|
+
authenticatorAttachment: discoverable.authenticatorAttachment,
|
|
153
|
+
passkey,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shouldFallbackToDiscoverable(error: unknown): boolean {
|
|
158
|
+
const name =
|
|
159
|
+
error && typeof error === 'object' && 'name' in error
|
|
160
|
+
? String((error as { name?: unknown }).name)
|
|
161
|
+
: '';
|
|
162
|
+
const message =
|
|
163
|
+
error && typeof error === 'object' && 'message' in error
|
|
164
|
+
? String((error as { message?: unknown }).message)
|
|
165
|
+
: '';
|
|
166
|
+
const normalized = `${name} ${message}`.toLowerCase();
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
normalized.includes('user rejected') ||
|
|
170
|
+
normalized.includes('user canceled') ||
|
|
171
|
+
normalized.includes('user cancelled')
|
|
172
|
+
) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
normalized.includes('notallowederror') ||
|
|
178
|
+
normalized.includes('invalidstateerror') ||
|
|
179
|
+
normalized.includes('notfounderror') ||
|
|
180
|
+
normalized.includes('not found') ||
|
|
181
|
+
normalized.includes('no passkey') ||
|
|
182
|
+
normalized.includes('no credential') ||
|
|
183
|
+
normalized.includes('saved for this app')
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
114
187
|
/**
|
|
115
188
|
* Sign with a discoverable passkey (no credential ID - browser prompts user to select).
|
|
116
189
|
*/
|
|
@@ -133,6 +206,7 @@ export async function signWithDiscoverablePasskey(
|
|
|
133
206
|
signatureS: result.signatureS,
|
|
134
207
|
credentialId: result.credentialId,
|
|
135
208
|
rpId: resolvedRpId,
|
|
209
|
+
authenticatorAttachment: result.authenticatorAttachment,
|
|
136
210
|
};
|
|
137
211
|
}
|
|
138
212
|
|
|
@@ -173,6 +247,7 @@ async function signWithPasskeyInline(
|
|
|
173
247
|
clientDataJSON: result.clientDataJSON,
|
|
174
248
|
signatureR: result.signatureR,
|
|
175
249
|
signatureS: result.signatureS,
|
|
250
|
+
authenticatorAttachment: result.authenticatorAttachment,
|
|
176
251
|
};
|
|
177
252
|
}
|
|
178
253
|
|
|
@@ -214,6 +289,17 @@ async function signWithPasskeyAssertion(
|
|
|
214
289
|
let { r, s } = parseDerSignature(signature);
|
|
215
290
|
s = normalizeLowS(s);
|
|
216
291
|
|
|
292
|
+
/* `authenticatorAttachment` distinguishes a same-device passkey
|
|
293
|
+
('platform') from a cross-device one signed via QR / hybrid
|
|
294
|
+
transport ('cross-platform'). Drives the wallet's add-device
|
|
295
|
+
prompt. Browsers may report null. */
|
|
296
|
+
const rawAttachment =
|
|
297
|
+
(
|
|
298
|
+
assertion as PublicKeyCredential & {
|
|
299
|
+
authenticatorAttachment?: AuthenticatorAttachment | null;
|
|
300
|
+
}
|
|
301
|
+
).authenticatorAttachment ?? null;
|
|
302
|
+
|
|
217
303
|
return {
|
|
218
304
|
signature: new Uint8Array([...r, ...s]),
|
|
219
305
|
authenticatorData: new Uint8Array(response.authenticatorData),
|
|
@@ -221,6 +307,7 @@ async function signWithPasskeyAssertion(
|
|
|
221
307
|
signatureR: r,
|
|
222
308
|
signatureS: s,
|
|
223
309
|
credentialId: arrayBufferToBase64Url(assertion.rawId),
|
|
310
|
+
authenticatorAttachment: rawAttachment,
|
|
224
311
|
};
|
|
225
312
|
}
|
|
226
313
|
|
|
@@ -259,13 +346,16 @@ async function requestStoredPasskeyPopup(
|
|
|
259
346
|
return decodePopupStoredSigningResult(result);
|
|
260
347
|
}
|
|
261
348
|
|
|
262
|
-
function decodePopupSigningResult(
|
|
349
|
+
function decodePopupSigningResult(
|
|
350
|
+
result: PasskeyPopupSigningResult
|
|
351
|
+
): PasskeySigningResult {
|
|
263
352
|
return {
|
|
264
353
|
signature: base64UrlToBytes(result.signatureBase64Url),
|
|
265
354
|
authenticatorData: base64UrlToBytes(result.authenticatorDataBase64Url),
|
|
266
355
|
clientDataJSON: base64UrlToBytes(result.clientDataJSONBase64Url),
|
|
267
356
|
signatureR: base64UrlToBytes(result.signatureRBase64Url),
|
|
268
357
|
signatureS: base64UrlToBytes(result.signatureSBase64Url),
|
|
358
|
+
authenticatorAttachment: result.authenticatorAttachment ?? null,
|
|
269
359
|
};
|
|
270
360
|
}
|
|
271
361
|
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PasskeySigningResult, PasskeyMetadata } from '@thru/passkey-manager';
|
|
1
|
+
import type { PasskeySigningResult, PasskeyMetadata } from '@thru/programs/passkey-manager';
|
|
2
2
|
|
|
3
3
|
// Re-export platform-agnostic types for backward compatibility
|
|
4
4
|
export type {
|
|
@@ -6,7 +6,7 @@ export type {
|
|
|
6
6
|
PasskeySigningResult,
|
|
7
7
|
PasskeyDiscoverableSigningResult,
|
|
8
8
|
PasskeyMetadata,
|
|
9
|
-
} from '@thru/passkey-manager';
|
|
9
|
+
} from '@thru/programs/passkey-manager';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Signing result with stored passkey metadata attached.
|
|
@@ -44,6 +44,25 @@ export interface PasskeyPopupContext {
|
|
|
44
44
|
imageUrl?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Options for stored passkey signing in embedded contexts.
|
|
49
|
+
*/
|
|
50
|
+
export interface PasskeyStoredSigningOptions {
|
|
51
|
+
allowPopupFallback?: boolean;
|
|
52
|
+
/** Prefer an RP-scoped discoverable credential prompt over a stored
|
|
53
|
+
* credential-id lookup. Native WebViews can show misleading
|
|
54
|
+
* app-level "no passkey" UI when allowCredentials is stale, while a
|
|
55
|
+
* discoverable prompt correctly lets the wallet/RP choose the passkey. */
|
|
56
|
+
preferDiscoverable?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for passkey registration in embedded contexts.
|
|
61
|
+
*/
|
|
62
|
+
export interface PasskeyRegistrationOptions {
|
|
63
|
+
allowPopupFallback?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/**
|
|
48
67
|
* Account info passed through popup bridge.
|
|
49
68
|
*/
|
|
@@ -95,6 +114,7 @@ export interface PasskeyPopupSigningResult {
|
|
|
95
114
|
clientDataJSONBase64Url: string;
|
|
96
115
|
signatureRBase64Url: string;
|
|
97
116
|
signatureSBase64Url: string;
|
|
117
|
+
authenticatorAttachment?: 'platform' | 'cross-platform' | null;
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
export interface PasskeyPopupStoredPasskey {
|
|
@@ -103,6 +123,10 @@ export interface PasskeyPopupStoredPasskey {
|
|
|
103
123
|
publicKeyY: string;
|
|
104
124
|
rpId: string;
|
|
105
125
|
label?: string;
|
|
126
|
+
deviceName?: string;
|
|
127
|
+
devicePlatform?: string;
|
|
128
|
+
browserName?: string;
|
|
129
|
+
authenticatorAttachment?: 'platform' | 'cross-platform' | null;
|
|
106
130
|
createdAt: string;
|
|
107
131
|
lastUsedAt: string;
|
|
108
132
|
}
|
|
@@ -117,6 +141,7 @@ export interface PasskeyPopupRegistrationResult {
|
|
|
117
141
|
publicKeyX: string;
|
|
118
142
|
publicKeyY: string;
|
|
119
143
|
rpId: string;
|
|
144
|
+
authenticatorAttachment?: 'platform' | 'cross-platform' | null;
|
|
120
145
|
}
|
|
121
146
|
|
|
122
147
|
export type PasskeyPopupResponse =
|
package/src/web.ts
CHANGED
|
@@ -7,9 +7,13 @@ export type {
|
|
|
7
7
|
PasskeyClientCapabilities,
|
|
8
8
|
PasskeyPopupContext,
|
|
9
9
|
PasskeyPopupAccount,
|
|
10
|
+
PasskeyStoredSigningOptions,
|
|
11
|
+
PasskeyRegistrationOptions,
|
|
10
12
|
} from './types';
|
|
11
13
|
|
|
12
14
|
export { registerPasskey } from './register';
|
|
15
|
+
export { createDistinctPasskeyLabel } from './label';
|
|
16
|
+
export type { DistinctPasskeyLabelOptions } from './label';
|
|
13
17
|
|
|
14
18
|
export {
|
|
15
19
|
signWithPasskey,
|
|
@@ -25,7 +29,7 @@ export {
|
|
|
25
29
|
P256_HALF_N,
|
|
26
30
|
bytesToBigIntBE,
|
|
27
31
|
bigIntToBytesBE,
|
|
28
|
-
} from '@thru/passkey-manager';
|
|
32
|
+
} from '@thru/programs/passkey-manager';
|
|
29
33
|
|
|
30
34
|
export {
|
|
31
35
|
isWebAuthnSupported,
|
|
@@ -48,4 +52,4 @@ export {
|
|
|
48
52
|
bytesEqual,
|
|
49
53
|
compareBytes,
|
|
50
54
|
uniqueAccounts,
|
|
51
|
-
} from '@thru/passkey-manager';
|
|
55
|
+
} from '@thru/programs/passkey-manager';
|
package/tsconfig.json
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
"outDir": "./dist",
|
|
5
5
|
"rootDir": "..",
|
|
6
6
|
"baseUrl": ".",
|
|
7
|
+
"ignoreDeprecations": "6.0",
|
|
7
8
|
"paths": {
|
|
8
|
-
"@thru/helpers": ["../
|
|
9
|
-
"@thru/passkey-manager": ["../passkey-manager/
|
|
9
|
+
"@thru/sdk/helpers": ["../sdk/src/helpers/index.ts"],
|
|
10
|
+
"@thru/programs/passkey-manager": ["../programs/src/passkey-manager/index.ts"]
|
|
10
11
|
}
|
|
11
12
|
},
|
|
12
13
|
"include": ["src/**/*"],
|
package/tsup.config.ts
CHANGED
|
@@ -8,6 +8,7 @@ export default defineConfig({
|
|
|
8
8
|
popup: 'src/popup-entry.ts',
|
|
9
9
|
mobile: 'src/mobile/index.ts',
|
|
10
10
|
auth: 'src/auth/index.ts',
|
|
11
|
+
'auth/add-device': 'src/auth/add-device.ts',
|
|
11
12
|
server: 'src/server/index.ts',
|
|
12
13
|
},
|
|
13
14
|
external: ['expo-secure-store', 'react-native', 'react-native-passkeys', 'zustand'],
|