@thru/passkey 0.2.21 → 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.
Files changed (79) hide show
  1. package/dist/auth/add-device.cjs +118 -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 +100 -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-75G2FPYW.js → chunk-OULTQZT7.js} +4 -3
  14. package/dist/chunk-OULTQZT7.js.map +1 -0
  15. package/dist/chunk-QAQNRQ7G.js +104 -0
  16. package/dist/chunk-QAQNRQ7G.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 +30 -60
  38. package/dist/server.cjs.map +1 -1
  39. package/dist/server.d.cts +2 -2
  40. package/dist/server.d.ts +2 -2
  41. package/dist/server.js +32 -62
  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 +213 -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 +2 -2
  66. package/src/server/create-wallet.test.ts +2 -2
  67. package/src/server/create-wallet.ts +24 -16
  68. package/src/server/submit.test.ts +2 -2
  69. package/src/server/submit.ts +25 -4
  70. package/src/server/types.ts +2 -2
  71. package/src/server/utils.ts +1 -1
  72. package/src/sign.ts +127 -37
  73. package/src/types.ts +27 -2
  74. package/src/web.ts +6 -2
  75. package/tsconfig.json +3 -2
  76. package/tsup.config.ts +1 -0
  77. package/dist/chunk-2JHC7OOH.js.map +0 -1
  78. package/dist/chunk-75G2FPYW.js.map +0 -1
  79. 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.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/passkey-manager": "0.2.21",
42
- "@thru/thru-sdk": "0.2.21"
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.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,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
+ }
@@ -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
 
@@ -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 './utils';
18
- import type { ThruClient } from './types';
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 ?? 'default';
30
+ const walletName = opts.walletName ?? "default";
31
31
  const seed = await createWalletSeed(walletName, opts.pubkeyX, opts.pubkeyY);
32
- const walletBytes = await deriveWalletAddress(seed, PASSKEY_MANAGER_PROGRAM_ADDRESS);
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 !== 'finalized') {
83
+ if (result.status !== "finalized") {
81
84
  throw new Error(
82
85
  `Wallet creation failed with status: ${result.status}${
83
- result.errorCode !== undefined ? ` (error code: ${result.errorCode})` : ''
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
- walletName,
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, walletName);
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(transaction.toWire());
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 !== 'finalized') {
150
+ if (result.status !== "finalized") {
145
151
  throw new Error(
146
152
  `Credential registration failed with status: ${result.status}${
147
- result.errorCode !== undefined ? ` (error code: ${result.errorCode})` : ''
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('Credential registration failed (non-fatal):', error);
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]),