@txnlab/use-wallet-passkey 0.0.0 → 5.2.0-next.3

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TxnLab, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,9 +1,115 @@
1
1
  # @txnlab/use-wallet-passkey
2
2
 
3
- This version is a placeholder. It reserves the package name so it can be published from CI with npm trusted publishing, and it contains no code.
3
+ [![npm version](https://img.shields.io/npm/v/@txnlab/use-wallet-passkey)](https://www.npmjs.com/package/@txnlab/use-wallet-passkey)
4
+ [![License](https://img.shields.io/github/license/TxnLab/use-wallet)](https://github.com/TxnLab/use-wallet/blob/main/LICENSE)
4
5
 
5
- The passkey wallet adapter for [use-wallet](https://github.com/TxnLab/use-wallet) is in prerelease:
6
+ Passkey adapter for [use-wallet](https://github.com/TxnLab/use-wallet), the Algorand wallet integration library.
7
+
8
+ It derives a **Falcon-1024 (post-quantum) account** from a passkey's WebAuthn PRF output. Users need no extension, app or seed phrase: their passkey provider (iCloud Keychain, Google Password Manager, 1Password, Windows Hello or a FIDO2 security key) stores and syncs the credential.
9
+
10
+ ## Installation
6
11
 
7
12
  ```bash
8
- npm install @txnlab/use-wallet-passkey@next
13
+ npm install @txnlab/use-wallet @txnlab/use-wallet-passkey
9
14
  ```
15
+
16
+ If you use a framework adapter (`@txnlab/use-wallet-react`, `-vue`, `-solid`, or `-svelte`), install it in place of `@txnlab/use-wallet`.
17
+
18
+ ## Usage
19
+
20
+ ```typescript
21
+ import { WalletManager } from '@txnlab/use-wallet'
22
+ import { passkey } from '@txnlab/use-wallet-passkey'
23
+
24
+ const manager = new WalletManager({
25
+ wallets: [passkey({ session: true })]
26
+ })
27
+ ```
28
+
29
+ Show two buttons. A missing passkey and a cancelled prompt raise the same browser error, so the adapter can't choose for the user:
30
+
31
+ ```typescript
32
+ await wallet.connect() // Sign in with an existing passkey
33
+ await wallet.connect({ create: true }) // Create a passkey (and a new account)
34
+ ```
35
+
36
+ Call `connect`, `signTransactions` and `signData` directly from the click handler, before awaiting anything else. Safari and every iOS browser drop the user's tap across an `await`, and the passkey prompt needs it.
37
+
38
+ ### Options
39
+
40
+ | Option | Default | |
41
+ | ---------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
42
+ | `rpId` | `location.hostname` | The domain the passkey, and so the account, is bound to. Set a parent domain (`example.com`) to share one account across its subdomains. |
43
+ | `rpName` | the RP ID | The name shown in passkey prompts |
44
+ | `context` | `''` | Separates this app's account from others on the same domain. No `:`; changing it changes the account. |
45
+ | `userName` | `<rpName> account` | The name new passkeys are saved under |
46
+ | `session` | `false` | Keep the key unlocked between signatures (see below). `true` or `{ idleTimeoutMs }`; the default timeout is 15 minutes, and `0` disables it. |
47
+ | `confirm` | `() => 'presence'` | How to confirm each signature while a session is unlocked |
48
+ | `keyStore` | built-in worker | Replace where the key is held (`PasskeyKeyStore`) |
49
+
50
+ The factory also accepts an optional `metadata` option to override the wallet's display name and icon.
51
+
52
+ ### Sessions and confirmation
53
+
54
+ Every passkey prompt that derives the key requires full user verification (biometric, PIN or system password), because the PRF output depends on it. Without `session`, every signing request runs one.
55
+
56
+ With `session`, only the first request after connecting or reloading does. The key then stays in a dedicated worker until the idle timeout, `disconnect` or a page reload, and `confirm` decides how each later request is approved:
57
+
58
+ - `'silent'`: no prompt
59
+ - `'presence'`: a passkey prompt with `userVerification: 'preferred'`, which some providers satisfy with a tap
60
+ - `'verify'`: a passkey prompt with full user verification
61
+
62
+ ```typescript
63
+ passkey({
64
+ session: { idleTimeoutMs: 10 * 60 * 1000 },
65
+ confirm: (request) =>
66
+ request.type === 'transactions' && request.transactions.some((txn) => txn.type === 'pay')
67
+ ? 'verify'
68
+ : 'presence'
69
+ })
70
+ ```
71
+
72
+ `confirm` must be synchronous: awaiting before the prompt would lose the user's tap on iOS.
73
+
74
+ ### Recovery phrase
75
+
76
+ The account is bound to your domain, so it's stranded if the domain is lost. Let users export its recovery phrase:
77
+
78
+ ```typescript
79
+ import type { PasskeyAdapter } from '@txnlab/use-wallet-passkey'
80
+
81
+ const wallet = manager.getWallet('passkey') as PasskeyAdapter
82
+ const { phrase } = await wallet.exportRecoveryPhrase() // fresh passkey prompt, full verification
83
+ ```
84
+
85
+ With a framework adapter, get the manager from `useWalletManager()`.
86
+
87
+ Tell users to **import the phrase as a Falcon / quantum account**: in Lute, Add account → "Falcon25 account" → Import; in Pera, the quantum import; or `algokey pq import --scheme falcon-1024`. The same 25 words in a regular import restore a different, empty account.
88
+
89
+ ### Content Security Policy
90
+
91
+ The key runs in a module worker started from a `blob:` URL, using WebAssembly. If your page sets a CSP, allow both:
92
+
93
+ ```
94
+ script-src 'self' 'wasm-unsafe-eval'; worker-src 'self' blob:
95
+ ```
96
+
97
+ Without them, signing fails with a `PasskeyError` whose `code` is `key-store-failed`.
98
+
99
+ ### Errors
100
+
101
+ Failures throw a `PasskeyError` with a `code`: `cancelled`, `unsupported` (no WebAuthn, or an in-app browser), `prf-unsupported` (the provider lacks the PRF extension; Ledger Stax and Dashlane on Android are known cases), `invalid-rp-id`, `key-mismatch`, `not-connected`, `key-store-failed` or `failed`. `signData` validation failures throw a `SignDataError` with an ARC-60 code.
102
+
103
+ ## Falcon accounts
104
+
105
+ - Each signed transaction carries a ~3.2 KB `pqsig` and pays an extra 2× the minimum fee. Get fees by simulating with `emptyTransactionSigner` and `allowEmptySignatures`.
106
+ - No multisig participation and no raw-bytes or program-data signing.
107
+ - `signData` supports ARC-60 authentication with scheme `falcon-1024-det`.
108
+
109
+ ## Security
110
+
111
+ - **The domain holds the keys.** Whoever controls the RP ID domain (including an XSS on it) can run a passkey prompt and recover the key with one tap.
112
+ - **Post-quantum is bounded by the passkey's custody.** The PRF itself is symmetric, but synced passkeys are protected by each provider's classical encryption, and the CTAP2 channel uses P-256. Hardware security keys that don't sync close more of that gap.
113
+ - Only public data is stored (credential ID, public key, address). The private key lives in a worker, never on the page's main thread, and is dropped on lock.
114
+
115
+ ### Visit [txnlab.gitbook.io/use-wallet](https://txnlab.gitbook.io/use-wallet) for docs, guides, and examples!
@@ -0,0 +1,224 @@
1
+ import algosdk from "algosdk";
2
+ import { AdapterConstructorParams, BaseWallet, Siwa, StdSignDataResponse, StdSignMetadata, WalletAccount, WalletMetadata } from "@txnlab/use-wallet/adapter";
3
+ import { WalletAdapterConfig, WalletFactoryOptions } from "@txnlab/use-wallet";
4
+ //#region src/key-store.d.ts
5
+ /**
6
+ * Holds the Falcon-1024 private key between a passkey's PRF ceremony and the
7
+ * signatures it authorizes. The adapter's built-in key store runs the key in a
8
+ * dedicated worker; supply your own (the `keyStore` option) to hold it
9
+ * elsewhere, e.g. a SharedWorker that outlives a page reload.
10
+ */
11
+ interface PasskeyKeyStore {
12
+ /** True when `sign` can run without a new PRF ceremony */
13
+ readonly unlocked: boolean;
14
+ /**
15
+ * Derive the key from the passkey's 32-byte PRF output and return its
16
+ * Falcon-1024 public key. Implementations must zero `entropy` and derive the
17
+ * keygen seed with `keygenSeedFromEntropy`.
18
+ */
19
+ unlock(entropy: Uint8Array): Promise<Uint8Array>;
20
+ /** Sign exactly these bytes with the private key */
21
+ sign(bytes: Uint8Array): Promise<Uint8Array>;
22
+ /** Drop the private key */
23
+ lock(): void;
24
+ }
25
+ interface WorkerKeyStoreOptions {
26
+ /** Clear the key after this long without a signature. 0 keeps it until `lock`. */
27
+ idleTimeoutMs?: number;
28
+ /** Called when the idle timeout clears the key */
29
+ onIdleLock?: () => void;
30
+ /** How long to wait for the worker to load. Default: 10000 */
31
+ startTimeoutMs?: number;
32
+ }
33
+ /**
34
+ * The built-in key store: a dedicated module worker started from a Blob URL.
35
+ * The worker module is inlined into this package, so a consumer's bundler has
36
+ * no worker entry to resolve; the page's CSP must allow `worker-src blob:` and
37
+ * `'wasm-unsafe-eval'`. `lock` terminates the worker, which drops its WASM
38
+ * heap.
39
+ */
40
+ export declare class WorkerKeyStore implements PasskeyKeyStore {
41
+ private readonly options;
42
+ private worker;
43
+ private isUnlocked;
44
+ private nextId;
45
+ private pending;
46
+ constructor(options?: WorkerKeyStoreOptions);
47
+ get unlocked(): boolean;
48
+ unlock(entropy: Uint8Array): Promise<Uint8Array>;
49
+ sign(bytes: Uint8Array): Promise<Uint8Array>;
50
+ lock(): void;
51
+ private start;
52
+ private startError;
53
+ private call;
54
+ private handle;
55
+ }
56
+ //#endregion
57
+ //#region src/adapter.d.ts
58
+ /**
59
+ * How to confirm a signing request while a session is unlocked:
60
+ * - `silent`: sign without a prompt
61
+ * - `presence`: a passkey prompt with `userVerification: 'preferred'`, which
62
+ * some providers satisfy with a tap and no biometric or system password
63
+ * - `verify`: a passkey prompt with `userVerification: 'required'`
64
+ *
65
+ * These prompts evaluate no PRF, so they never change the account. When the
66
+ * session is locked, the PRF ceremony that unlocks it is the confirmation.
67
+ */
68
+ type PasskeyConfirmation = 'silent' | 'presence' | 'verify';
69
+ type PasskeySigningRequest = {
70
+ type: 'transactions';
71
+ transactions: algosdk.Transaction[];
72
+ } | {
73
+ type: 'data';
74
+ siwa: Siwa;
75
+ };
76
+ interface PasskeySessionOptions {
77
+ /** Lock the session after this long without a signature; 0 never locks it. Default: 15 minutes */
78
+ idleTimeoutMs?: number;
79
+ }
80
+ interface PasskeyOptions {
81
+ /**
82
+ * The WebAuthn relying party ID: the domain the passkey (and so the account)
83
+ * is bound to. Default: `location.hostname`. Set a parent domain (e.g.
84
+ * `example.com` for `app.example.com`) to share one account across its
85
+ * subdomains.
86
+ */
87
+ rpId?: string;
88
+ /** The relying party name shown in passkey prompts. Default: the RP ID */
89
+ rpName?: string;
90
+ /**
91
+ * Separates this app's account from others on the same RP ID. Up to 128
92
+ * characters, no `:`. Default: `''`. Changing it changes the account.
93
+ */
94
+ context?: string;
95
+ /** The user name new passkeys are saved under. Default: `<rpName> account` */
96
+ userName?: string;
97
+ /**
98
+ * Keep the key unlocked between signatures, so only the first signature
99
+ * (or `connect`) runs a PRF ceremony with full user verification; later ones
100
+ * are confirmed per `confirm`. The key is held in a dedicated worker until
101
+ * the idle timeout, `disconnect` or a page reload. Default: `false`, a PRF
102
+ * ceremony for every request.
103
+ */
104
+ session?: boolean | PasskeySessionOptions;
105
+ /**
106
+ * Chooses how to confirm each signing request while a session is unlocked.
107
+ * Called synchronously, before the prompt: WebKit drops the user gesture
108
+ * across an await, so this must not be async. Default: `() => 'presence'`.
109
+ */
110
+ confirm?: (request: PasskeySigningRequest) => PasskeyConfirmation;
111
+ /**
112
+ * Replaces the built-in worker key store, e.g. to keep a session across page
113
+ * reloads. The adapter treats a custom key store as a session: it never
114
+ * locks it after signing, only on `disconnect`. May be called more than once.
115
+ */
116
+ keyStore?: () => PasskeyKeyStore;
117
+ }
118
+ interface PasskeyConnectArgs {
119
+ /** Create a new passkey (and account) instead of signing in with an existing one */
120
+ create?: boolean;
121
+ /** The user name to save a new passkey under */
122
+ userName?: string;
123
+ }
124
+ interface PasskeyRecoveryPhrase {
125
+ /** Import the phrase as a Falcon-1024 account, not a regular 25-word account */
126
+ scheme: 'falcon-1024';
127
+ /** The 25-word phrase */
128
+ phrase: string;
129
+ }
130
+ export declare class PasskeyAdapter extends BaseWallet<PasskeyOptions> {
131
+ private keyStore;
132
+ constructor(params: AdapterConstructorParams<PasskeyOptions>);
133
+ static defaultMetadata: WalletMetadata;
134
+ private get rpId();
135
+ private get rpName();
136
+ private get context();
137
+ /** True when the key stays unlocked between requests */
138
+ private get isSession();
139
+ private createKeyStore;
140
+ private getKeyStore;
141
+ /** Drop the key after a request, unless a session keeps it */
142
+ private release;
143
+ /**
144
+ * Sign in with an existing passkey, or create one with `{ create: true }`.
145
+ * Show both choices: a missing passkey and a cancelled prompt raise the same
146
+ * error, so the adapter can't tell which one happened.
147
+ */
148
+ connect: (args?: PasskeyConnectArgs) => Promise<WalletAccount[]>;
149
+ disconnect: () => Promise<void>;
150
+ /** Restores the account without a ceremony; the key unlocks on the first signature */
151
+ resumeSession: () => Promise<void>;
152
+ private matchesRecord;
153
+ private requireRecord;
154
+ /**
155
+ * Make sure the key is unlocked and the user approved this request. When the
156
+ * key is locked, the PRF ceremony that unlocks it is the approval. Starts its
157
+ * prompt before any await.
158
+ */
159
+ private authorize;
160
+ private processTxns;
161
+ private processEncodedTxns;
162
+ /**
163
+ * Signs the transactions sent from the connected account. Each one carries a
164
+ * Falcon-1024 `pqsig` (about 3.2 KB) and pays an extra 2× the minimum fee;
165
+ * get fees by simulating with `emptyTransactionSigner`.
166
+ */
167
+ signTransactions: <T extends algosdk.Transaction[] | Uint8Array[]>(txnGroup: T | T[], indexesToSign?: number[]) => Promise<(Uint8Array | null)[]>;
168
+ canSignData: boolean;
169
+ /**
170
+ * Signs an ARC-60 authentication request (SIWA) with the account's Falcon
171
+ * key, scheme `falcon-1024-det`. The signature covers
172
+ * `SHA-256(data) || SHA-256(authenticatorData)`.
173
+ */
174
+ signData: (data: string, metadata: StdSignMetadata) => Promise<StdSignDataResponse>;
175
+ /**
176
+ * Returns the account's 25-word recovery phrase after a fresh passkey
177
+ * ceremony with full user verification. Show it with a warning that it must
178
+ * be imported as a Falcon / quantum account (Lute: "Falcon25 account";
179
+ * Pera: quantum import; `algokey pq import --scheme falcon-1024`): the same
180
+ * words in a regular 25-word import restore a different, empty account.
181
+ */
182
+ exportRecoveryPhrase: () => Promise<PasskeyRecoveryPhrase>;
183
+ }
184
+ //#endregion
185
+ //#region src/errors.d.ts
186
+ type PasskeyErrorCode =
187
+ /** The user cancelled or the request timed out (or, on iOS, an in-app browser blocked it) */
188
+ 'cancelled' |
189
+ /** This browser can't run passkey ceremonies (no WebAuthn, or an Android WebView) */
190
+ 'unsupported' |
191
+ /** The passkey's provider doesn't support the PRF extension */
192
+ 'prf-unsupported' |
193
+ /** The relying party ID isn't valid for this page's origin */
194
+ 'invalid-rp-id' |
195
+ /** The passkey derived a different key than the connected account's */
196
+ 'key-mismatch' |
197
+ /** No account is connected */
198
+ 'not-connected' |
199
+ /** The key store failed, e.g. the session worker was blocked by a CSP */
200
+ 'key-store-failed' | 'failed';
201
+ export declare class PasskeyError extends Error {
202
+ readonly code: PasskeyErrorCode;
203
+ constructor(code: PasskeyErrorCode, message: string, options?: {
204
+ cause?: unknown;
205
+ });
206
+ }
207
+ //#endregion
208
+ //#region src/derivation.d.ts
209
+ /** The PRF input prefix. Frozen: changing it moves every account. */
210
+ export declare const PRF_LABEL_PREFIX = "@txnlab/use-wallet-passkey:falcon-1024:v1:";
211
+ /** The PRF input (`eval.first`) for an account context */
212
+ export declare function prfInput(context: string): Uint8Array;
213
+ /** keygenSeed = SHA-512/256("PQK" || "f1" || entropy) */
214
+ export declare function keygenSeedFromEntropy(entropy: Uint8Array): Uint8Array;
215
+ //#endregion
216
+ //#region src/record.d.ts
217
+ export declare const LOCAL_STORAGE_PASSKEY_KEY = "@txnlab/use-wallet:v5:passkey";
218
+ //#endregion
219
+ //#region src/index.d.ts
220
+ export declare const WALLET_ID: "passkey";
221
+ export declare function passkey(options?: PasskeyOptions & WalletFactoryOptions): WalletAdapterConfig;
222
+ //#endregion
223
+ export type { PasskeyConfirmation, PasskeyConnectArgs, PasskeyErrorCode, PasskeyKeyStore, PasskeyOptions, PasskeyRecoveryPhrase, PasskeySessionOptions, PasskeySigningRequest, WorkerKeyStoreOptions };
224
+ //# sourceMappingURL=index.d.ts.map