@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.
Files changed (80) hide show
  1. package/dist/auth/add-device.cjs +139 -0
  2. package/dist/auth/add-device.cjs.map +1 -0
  3. package/dist/auth/add-device.d.cts +69 -0
  4. package/dist/auth/add-device.d.ts +69 -0
  5. package/dist/auth/add-device.js +7 -0
  6. package/dist/auth/add-device.js.map +1 -0
  7. package/dist/auth.cjs +121 -6
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.d.cts +2 -0
  10. package/dist/auth.d.ts +2 -0
  11. package/dist/auth.js +10 -4
  12. package/dist/auth.js.map +1 -1
  13. package/dist/chunk-KASTJBBY.js +128 -0
  14. package/dist/chunk-KASTJBBY.js.map +1 -0
  15. package/dist/{chunk-75G2FPYW.js → chunk-OULTQZT7.js} +4 -3
  16. package/dist/chunk-OULTQZT7.js.map +1 -0
  17. package/dist/{chunk-B5SN7AS7.js → chunk-TW7HANJM.js} +99 -49
  18. package/dist/chunk-TW7HANJM.js.map +1 -0
  19. package/dist/{chunk-2JHC7OOH.js → chunk-ZNBMADOM.js} +2 -2
  20. package/dist/chunk-ZNBMADOM.js.map +1 -0
  21. package/dist/index.cjs +102 -50
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +3 -3
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +4 -2
  26. package/dist/mobile.cjs +2 -2
  27. package/dist/mobile.cjs.map +1 -1
  28. package/dist/mobile.d.cts +2 -2
  29. package/dist/mobile.d.ts +2 -2
  30. package/dist/mobile.js +2 -2
  31. package/dist/mobile.js.map +1 -1
  32. package/dist/popup.cjs +3 -2
  33. package/dist/popup.cjs.map +1 -1
  34. package/dist/popup.d.cts +3 -3
  35. package/dist/popup.d.ts +3 -3
  36. package/dist/popup.js +1 -1
  37. package/dist/server.cjs +54 -66
  38. package/dist/server.cjs.map +1 -1
  39. package/dist/server.d.cts +14 -6
  40. package/dist/server.d.ts +14 -6
  41. package/dist/server.js +58 -69
  42. package/dist/server.js.map +1 -1
  43. package/dist/{types-_HRzmn-j.d.cts → types-BTTlCVrw.d.cts} +25 -2
  44. package/dist/{types-_HRzmn-j.d.ts → types-BTTlCVrw.d.ts} +25 -2
  45. package/dist/web.cjs +99 -48
  46. package/dist/web.cjs.map +1 -1
  47. package/dist/web.d.cts +14 -7
  48. package/dist/web.d.ts +14 -7
  49. package/dist/web.js +3 -1
  50. package/package.json +11 -6
  51. package/src/auth/add-device.ts +236 -0
  52. package/src/auth/execute-tx.ts +1 -1
  53. package/src/auth/index.ts +11 -0
  54. package/src/auth/use-passkey-auth.ts +4 -2
  55. package/src/capabilities.ts +2 -1
  56. package/src/index.ts +4 -0
  57. package/src/label.test.ts +21 -0
  58. package/src/label.ts +14 -0
  59. package/src/mobile/index.ts +1 -1
  60. package/src/mobile/passkey.ts +1 -1
  61. package/src/mobile/storage.ts +1 -1
  62. package/src/mobile/types.ts +2 -2
  63. package/src/popup-service.ts +2 -1
  64. package/src/register.ts +23 -8
  65. package/src/server/challenge.ts +15 -4
  66. package/src/server/create-wallet.test.ts +2 -2
  67. package/src/server/create-wallet.ts +24 -16
  68. package/src/server/handlers.ts +6 -2
  69. package/src/server/submit.test.ts +24 -6
  70. package/src/server/submit.ts +41 -10
  71. package/src/server/types.ts +5 -3
  72. package/src/server/utils.ts +1 -1
  73. package/src/sign.ts +127 -37
  74. package/src/types.ts +27 -2
  75. package/src/web.ts +6 -2
  76. package/tsconfig.json +3 -2
  77. package/tsup.config.ts +1 -0
  78. package/dist/chunk-2JHC7OOH.js.map +0 -1
  79. package/dist/chunk-75G2FPYW.js.map +0 -1
  80. 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.21",
3
+ "version": "0.2.23",
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/passkey-manager": "0.2.21",
42
- "@thru/thru-sdk": "0.2.21"
46
+ "@thru/sdk": "0.2.23",
47
+ "@thru/programs": "0.2.23"
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.0",
66
- "typescript": "^5.9.3",
67
- "vitest": "^3.2.4"
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,236 @@
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))
8
+ or VALIDATE(existingAuthority -> multicall[ADD_AUTHORITY, REGISTER_CREDENTIAL])
9
+ asks the caller's existing passkey to sign the challenge, then asks
10
+ the caller's wallet signer to sign the assembled transaction, sends
11
+ it, and returns the result. */
12
+
13
+ import {
14
+ type Authority,
15
+ buildAccountContext,
16
+ createValidateChallenge,
17
+ createCredentialLookupSeed,
18
+ decodeAddress,
19
+ deriveWalletAddress,
20
+ encodeAddAuthorityInstruction,
21
+ encodeRegisterCredentialInstruction,
22
+ encodeValidateInstruction,
23
+ parseWalletAuthorities,
24
+ type ParsedAuthority,
25
+ type WalletSigner,
26
+ } from "@thru/programs/passkey-manager";
27
+ import {
28
+ MULTICALL_PROGRAM_PUBKEY,
29
+ buildMulticallInstruction,
30
+ } from "@thru/programs/multicall";
31
+
32
+ /** Minimal shape required from a passkey signer. Both web's
33
+ `signWithDiscoverablePasskey`/`signWithPasskey` and mobile's
34
+ counterparts conform. */
35
+ export interface PasskeyChallengeSigner {
36
+ signChallenge: (challenge: Uint8Array) => Promise<{
37
+ signatureR: Uint8Array;
38
+ signatureS: Uint8Array;
39
+ authenticatorData: Uint8Array;
40
+ clientDataJSON: Uint8Array;
41
+ }>;
42
+ }
43
+
44
+ /** Minimal shape required from a Thru chain client. Loosely-typed
45
+ because @thru/sdk's DTS emit is currently broken in this
46
+ repo. The caller passes the real Thru and we narrow operationally. */
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ export type AnyThruClient = any;
49
+
50
+ export interface AddDeviceParams {
51
+ /** Loosely-typed Thru chain client (`@thru/sdk/client`). */
52
+ thru: AnyThruClient;
53
+ /** Wallet (the on-chain WalletAccount) to attach the passkey to. */
54
+ walletAddress: string;
55
+ /** Index of the existing authority that approves this change. Must
56
+ currently be a passkey authority. */
57
+ authIdx: number;
58
+ /** New passkey to attach. tag = 1 (passkey). */
59
+ newAuthority: Authority;
60
+ /** Optional credential-lookup registration so the new passkey is
61
+ discoverable on subsequent sign-ins. */
62
+ credentialId?: Uint8Array;
63
+ walletName?: string;
64
+ /** Existing-passkey challenge signer (web or mobile). */
65
+ passkey: PasskeyChallengeSigner;
66
+ /** Wallet transaction signer that returns base64(signed bytes). */
67
+ walletSigner: WalletSigner;
68
+ /** Passkey program address (base58). */
69
+ programAddress: string;
70
+ /** Sign-and-send executor (lifted from passkey-transaction.ts in the
71
+ wallet-auth-manager - wallet apps own this because it depends on
72
+ the `Thru` client's transaction builder). */
73
+ executor: TxExecutor;
74
+ /** Optional status callback so UIs can show progress. */
75
+ onStatus?: (message: string) => void;
76
+ }
77
+
78
+ export interface TxExecutorParams {
79
+ thru: AnyThruClient;
80
+ walletSigner: WalletSigner;
81
+ instructionData: Uint8Array;
82
+ readWriteAddresses: string[];
83
+ readOnlyAddresses: string[];
84
+ label: string;
85
+ }
86
+
87
+ export interface TxExecutorResult {
88
+ signature: string;
89
+ /** Loosely-typed because @thru/sdk types aren't available. */
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ execution: any;
92
+ }
93
+
94
+ export type TxExecutor = (
95
+ params: TxExecutorParams,
96
+ ) => Promise<TxExecutorResult>;
97
+
98
+ export interface AddDeviceResult extends TxExecutorResult {
99
+ /** The new passkey's authority index after the transaction lands. */
100
+ newAuthorityIdx: number;
101
+ }
102
+
103
+ /**
104
+ * Run VALIDATE + ADD_AUTHORITY [+ REGISTER_CREDENTIAL] to attach a new
105
+ * passkey to an on-chain WalletAccount.
106
+ */
107
+ export async function addDeviceToAccount(
108
+ params: AddDeviceParams,
109
+ ): Promise<AddDeviceResult> {
110
+ const status = params.onStatus ?? (() => {});
111
+
112
+ const walletAccount = await params.thru.accounts.get(params.walletAddress);
113
+ const walletData: Uint8Array | undefined = walletAccount?.data?.data;
114
+ if (!walletData) {
115
+ throw new Error("Wallet account data missing");
116
+ }
117
+
118
+ const parsed = parseWalletAuthorities(walletData);
119
+ const authorizing: ParsedAuthority | undefined =
120
+ parsed.authorities[params.authIdx];
121
+ if (!authorizing) {
122
+ throw new Error("Authorization index out of bounds");
123
+ }
124
+ if (authorizing.kind !== "passkey") {
125
+ throw new Error(
126
+ "addDeviceToAccount currently requires a passkey authority for VALIDATE",
127
+ );
128
+ }
129
+
130
+ /* The new authority will land at the next free slot. */
131
+ const newAuthorityIdx = parsed.authorities.length;
132
+
133
+ let readWriteAccounts: Uint8Array[] = [];
134
+ let lookupSeed: Uint8Array | undefined;
135
+ let lookupAddressBytes: Uint8Array | undefined;
136
+ let lookupProof: Uint8Array | undefined;
137
+
138
+ if (params.credentialId) {
139
+ lookupSeed = await createCredentialLookupSeed(params.credentialId);
140
+ lookupAddressBytes = await deriveWalletAddress(
141
+ lookupSeed,
142
+ params.programAddress,
143
+ );
144
+
145
+ status("Fetching state proof for credential lookup...");
146
+ const proofResult = await params.thru.proofs.generate({
147
+ proofType: 1 /* StateProofType.CREATING */,
148
+ address: lookupAddressBytes,
149
+ });
150
+ lookupProof = proofResult.proof;
151
+ readWriteAccounts = [lookupAddressBytes];
152
+ }
153
+
154
+ const ctx = buildAccountContext({
155
+ walletAddress: params.walletAddress,
156
+ readWriteAccounts,
157
+ readOnlyAccounts: params.credentialId ? [MULTICALL_PROGRAM_PUBKEY] : [],
158
+ programAddress: params.programAddress,
159
+ });
160
+
161
+ const passkeyProgramPubkey = decodeAddress(params.programAddress);
162
+ const addAuthorityInstruction = encodeAddAuthorityInstruction({
163
+ walletAccountIdx: ctx.walletAccountIdx,
164
+ authority: params.newAuthority,
165
+ });
166
+
167
+ let targetProgramIdx = ctx.getAccountIndex(passkeyProgramPubkey);
168
+ let targetInstructionData = addAuthorityInstruction;
169
+
170
+ if (params.credentialId) {
171
+ if (!lookupSeed || !lookupAddressBytes || !lookupProof) {
172
+ throw new Error("Credential lookup proof data missing");
173
+ }
174
+
175
+ const registerCredentialInstruction = encodeRegisterCredentialInstruction({
176
+ walletAccountIdx: ctx.walletAccountIdx,
177
+ lookupAccountIdx: ctx.getAccountIndex(lookupAddressBytes),
178
+ seed: lookupSeed,
179
+ stateProof: lookupProof,
180
+ });
181
+
182
+ targetProgramIdx = ctx.getAccountIndex(MULTICALL_PROGRAM_PUBKEY);
183
+ targetInstructionData = buildMulticallInstruction([
184
+ {
185
+ programIdx: ctx.getAccountIndex(passkeyProgramPubkey),
186
+ instructionData: addAuthorityInstruction,
187
+ },
188
+ {
189
+ programIdx: ctx.getAccountIndex(passkeyProgramPubkey),
190
+ instructionData: registerCredentialInstruction,
191
+ },
192
+ ]);
193
+ }
194
+
195
+ /* Build the VALIDATE challenge over the target CPI and ask the caller's passkey to sign. */
196
+ const challenge = await createValidateChallenge(
197
+ parsed.nonce,
198
+ ctx.accountAddresses,
199
+ ctx.walletAccountIdx,
200
+ params.authIdx,
201
+ {
202
+ programIdx: targetProgramIdx,
203
+ instructionData: targetInstructionData,
204
+ },
205
+ );
206
+
207
+ status("Waiting for passkey approval...");
208
+ const signature = await params.passkey.signChallenge(challenge);
209
+
210
+ const validateInstruction = encodeValidateInstruction({
211
+ walletAccountIdx: ctx.walletAccountIdx,
212
+ authIdx: params.authIdx,
213
+ targetInstruction: {
214
+ programIdx: targetProgramIdx,
215
+ instructionData: targetInstructionData,
216
+ },
217
+ signatureR: signature.signatureR,
218
+ signatureS: signature.signatureS,
219
+ authenticatorData: signature.authenticatorData,
220
+ clientDataJSON: signature.clientDataJSON,
221
+ });
222
+
223
+ status("Sending transaction...");
224
+ const result = await params.executor({
225
+ thru: params.thru,
226
+ walletSigner: params.walletSigner,
227
+ instructionData: validateInstruction,
228
+ readWriteAddresses: ctx.readWriteAddresses,
229
+ readOnlyAddresses: ctx.readOnlyAddresses,
230
+ label: params.credentialId
231
+ ? "VALIDATE -> MULTICALL(ADD_AUTHORITY, REGISTER_CREDENTIAL)"
232
+ : "VALIDATE -> ADD_AUTHORITY",
233
+ });
234
+
235
+ return { ...result, newAuthorityIdx };
236
+ }
@@ -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(resolvedAlias, tempId, {
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
  });
@@ -1,6 +1,7 @@
1
1
  import type { PasskeyClientCapabilities } from './types';
2
2
 
3
- const DEBUG = typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_PASSKEY_DEBUG === '1';
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
+ }
@@ -9,7 +9,7 @@ export type {
9
9
 
10
10
  export { classifyPasskeyError, type PasskeyErrorKind } from './errors';
11
11
 
12
- export { bytesToBase64 } from '@thru/passkey-manager';
12
+ export { bytesToBase64 } from '@thru/programs/passkey-manager';
13
13
 
14
14
  export {
15
15
  storePasskeyMetadata,
@@ -5,7 +5,7 @@ import {
5
5
  normalizeLowS,
6
6
  parseDerSignature,
7
7
  type PasskeySigningResult,
8
- } from '@thru/passkey-manager';
8
+ } from '@thru/programs/passkey-manager';
9
9
  import type {
10
10
  DiscoverablePasskeyResult,
11
11
  PasskeyMobileConfig,
@@ -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,
@@ -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;
@@ -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 { PasskeyRegistrationResult, PasskeyPopupRegistrationResult } from './types';
2
- import { arrayBufferToBase64Url, bytesToHex } from '@thru/passkey-manager';
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 preopenedPopup = maybePreopenPopup(action, openPasskeyPopupWindow);
37
- const promptMode = await getPasskeyPromptMode(action);
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
 
@@ -1,22 +1,33 @@
1
1
  import {
2
2
  bytesToBase64Url,
3
3
  createValidateChallenge,
4
+ decodeAddress,
4
5
  fetchWalletNonce,
5
- } from '@thru/passkey-manager';
6
- import type { AccountContext } from '@thru/passkey-manager';
6
+ } from '@thru/programs/passkey-manager';
7
+ import type { AccountContext } from '@thru/programs/passkey-manager';
7
8
  import type { PasskeyChallengeResult, ThruClient } from './types';
8
9
 
9
10
  export async function createPasskeyChallenge(opts: {
10
11
  client: ThruClient;
11
12
  walletAddress: string;
12
13
  accountCtx: AccountContext;
13
- invokeIx: Uint8Array;
14
+ targetProgramAddress: string;
15
+ instructionData: Uint8Array;
16
+ authIdx?: number;
14
17
  }): Promise<PasskeyChallengeResult> {
15
18
  const nonce = await fetchWalletNonce(opts.client, opts.walletAddress);
19
+ const targetProgramIdx = opts.accountCtx.getAccountIndex(
20
+ decodeAddress(opts.targetProgramAddress)
21
+ );
16
22
  const challenge = await createValidateChallenge(
17
23
  nonce,
18
24
  opts.accountCtx.accountAddresses,
19
- opts.invokeIx
25
+ opts.accountCtx.walletAccountIdx,
26
+ opts.authIdx ?? 0,
27
+ {
28
+ programIdx: targetProgramIdx,
29
+ instructionData: opts.instructionData,
30
+ }
20
31
  );
21
32
 
22
33
  return {
@@ -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[] }) => ({