kentucky-signer-viem 0.1.1 → 0.1.4

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/README.md CHANGED
@@ -1,17 +1,20 @@
1
1
  # kentucky-signer-viem
2
2
 
3
- A custom Viem account integration for the Kentucky Signer service, enabling EVM transaction signing using passkey (WebAuthn) or password authentication.
3
+ A custom Viem account integration for the Kentucky Signer service, enabling EVM transaction signing using passkey (WebAuthn) or password authentication with optional two-factor authentication (TOTP/PIN).
4
4
 
5
5
  ## Features
6
6
 
7
- - Custom Viem account backed by Kentucky Signer
8
- - Passkey (WebAuthn) authentication for browser environments
9
- - Password authentication for browser and Node.js environments
10
- - JWT token authentication for Node.js/server environments
11
- - Account creation with passkey or password
12
- - React hooks and context for easy integration
13
- - TypeScript support with full type definitions
14
- - Session management with automatic refresh
7
+ - **Custom Viem Account** - Full Viem compatibility for signing transactions, messages, and typed data
8
+ - **Multiple Authentication Methods**
9
+ - Passkey (WebAuthn) for browser environments
10
+ - Password authentication for browser and Node.js
11
+ - JWT token authentication for server environments
12
+ - **Secure Mode** - Ephemeral key signing with client-side key generation
13
+ - **Two-Factor Authentication** - TOTP (authenticator app) and PIN support
14
+ - **Guardian Recovery** - Social recovery with trusted guardians
15
+ - **React Integration** - Hooks and context for easy React app integration
16
+ - **TypeScript Support** - Full type definitions included
17
+ - **Session Management** - Automatic refresh and persistence options
15
18
 
16
19
  ## Installation
17
20
 
@@ -63,67 +66,36 @@ const hash = await walletClient.sendTransaction({
63
66
  to: '0x...',
64
67
  value: parseEther('0.1'),
65
68
  })
66
- console.log('Transaction hash:', hash)
67
69
  ```
68
70
 
69
- ### Password Authentication (Browser or Node.js)
71
+ ### Password Authentication
70
72
 
71
73
  ```typescript
72
- import { createWalletClient, http, parseEther } from 'viem'
73
- import { mainnet } from 'viem/chains'
74
74
  import {
75
- createKentuckySignerAccount,
76
75
  authenticateWithPassword,
77
76
  createAccountWithPassword,
78
77
  } from 'kentucky-signer-viem'
79
78
 
80
- // Option 1: Create a new account with password
79
+ // Create a new account
81
80
  const newAccount = await createAccountWithPassword({
82
81
  baseUrl: 'https://signer.example.com',
83
82
  password: 'your-secure-password',
84
83
  confirmation: 'your-secure-password',
85
84
  })
86
- console.log('Account ID:', newAccount.account_id)
87
- console.log('EVM Address:', newAccount.addresses.evm)
88
85
 
89
- // Option 2: Authenticate with existing account
86
+ // Or authenticate with existing account
90
87
  const session = await authenticateWithPassword({
91
88
  baseUrl: 'https://signer.example.com',
92
- accountId: newAccount.account_id,
89
+ accountId: 'existing_account_id',
93
90
  password: 'your-secure-password',
94
91
  })
95
-
96
- // Create Kentucky Signer account
97
- const account = createKentuckySignerAccount({
98
- config: {
99
- baseUrl: 'https://signer.example.com',
100
- accountId: session.accountId,
101
- },
102
- session,
103
- defaultChainId: 1,
104
- })
105
-
106
- // Use with Viem
107
- const walletClient = createWalletClient({
108
- account,
109
- chain: mainnet,
110
- transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
111
- })
112
-
113
- const hash = await walletClient.sendTransaction({
114
- to: '0x...',
115
- value: parseEther('0.1'),
116
- })
117
92
  ```
118
93
 
119
94
  ### Node.js (with JWT Token)
120
95
 
121
96
  ```typescript
122
- import { createWalletClient, http, parseEther } from 'viem'
123
- import { mainnet } from 'viem/chains'
124
97
  import { createServerAccount } from 'kentucky-signer-viem'
125
98
 
126
- // Create account with existing JWT token
127
99
  const account = createServerAccount(
128
100
  'https://signer.example.com',
129
101
  'account_id_hex',
@@ -131,18 +103,6 @@ const account = createServerAccount(
131
103
  '0xYourEvmAddress',
132
104
  1 // chainId
133
105
  )
134
-
135
- // Use with Viem
136
- const walletClient = createWalletClient({
137
- account,
138
- chain: mainnet,
139
- transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY'),
140
- })
141
-
142
- const hash = await walletClient.sendTransaction({
143
- to: '0x...',
144
- value: parseEther('0.1'),
145
- })
146
106
  ```
147
107
 
148
108
  ## React Integration
@@ -158,6 +118,7 @@ function App() {
158
118
  baseUrl="https://signer.example.com"
159
119
  defaultChainId={1}
160
120
  persistSession={true}
121
+ useEphemeralKeys={true} // Enable secure mode
161
122
  >
162
123
  <YourApp />
163
124
  </KentuckySignerProvider>
@@ -168,29 +129,24 @@ function App() {
168
129
  ### Authentication Hook
169
130
 
170
131
  ```tsx
171
- import { useKentuckySigner, usePasskeyAuth } from 'kentucky-signer-viem/react'
132
+ import { useKentuckySigner } from 'kentucky-signer-viem/react'
172
133
 
173
134
  function LoginButton() {
174
- const { isAuthenticated, account } = useKentuckySigner()
175
- const { login, isLoading, error } = usePasskeyAuth()
176
- const [accountId, setAccountId] = useState('')
135
+ const { isAuthenticated, account, authenticate, logout } = useKentuckySigner()
177
136
 
178
137
  if (isAuthenticated && account) {
179
- return <div>Connected: {account.address}</div>
138
+ return (
139
+ <div>
140
+ <span>Connected: {account.address}</span>
141
+ <button onClick={logout}>Logout</button>
142
+ </div>
143
+ )
180
144
  }
181
145
 
182
146
  return (
183
- <div>
184
- <input
185
- placeholder="Account ID"
186
- value={accountId}
187
- onChange={(e) => setAccountId(e.target.value)}
188
- />
189
- <button onClick={() => login(accountId)} disabled={isLoading}>
190
- {isLoading ? 'Authenticating...' : 'Login with Passkey'}
191
- </button>
192
- {error && <div className="error">{error.message}</div>}
193
- </div>
147
+ <button onClick={() => authenticate('account_id')}>
148
+ Login with Passkey
149
+ </button>
194
150
  )
195
151
  }
196
152
  ```
@@ -200,159 +156,173 @@ function LoginButton() {
200
156
  ```tsx
201
157
  import { useWalletClient, useIsReady } from 'kentucky-signer-viem/react'
202
158
  import { mainnet } from 'viem/chains'
203
- import { parseEther } from 'viem'
204
159
 
205
160
  function SendTransaction() {
206
161
  const isReady = useIsReady()
207
- const walletClient = useWalletClient({
208
- chain: mainnet,
209
- rpcUrl: 'https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY',
210
- })
162
+ const walletClient = useWalletClient({ chain: mainnet })
211
163
 
212
164
  async function send() {
213
165
  if (!walletClient) return
214
-
215
166
  const hash = await walletClient.sendTransaction({
216
167
  to: '0x...',
217
168
  value: parseEther('0.1'),
218
169
  })
219
- console.log('Transaction hash:', hash)
220
170
  }
221
171
 
222
- return (
223
- <button onClick={send} disabled={!isReady}>
224
- Send 0.1 ETH
225
- </button>
226
- )
172
+ return <button onClick={send} disabled={!isReady}>Send</button>
227
173
  }
228
174
  ```
229
175
 
230
- ### Sign Message Hook
231
-
232
- ```tsx
233
- import { useSignMessage } from 'kentucky-signer-viem/react'
234
-
235
- function SignMessageDemo() {
236
- const { signMessage, isLoading, isAvailable } = useSignMessage()
237
- const [signature, setSignature] = useState('')
238
-
239
- async function sign() {
240
- const sig = await signMessage('Hello, Kentucky Signer!')
241
- setSignature(sig)
242
- }
176
+ ## Secure Mode (Ephemeral Keys)
243
177
 
244
- return (
245
- <div>
246
- <button onClick={sign} disabled={isLoading || !isAvailable}>
247
- {isLoading ? 'Signing...' : 'Sign Message'}
248
- </button>
249
- {signature && <pre>{signature}</pre>}
250
- </div>
251
- )
252
- }
253
- ```
178
+ Secure mode adds an extra layer of security by requiring client-side ephemeral key signatures for all operations:
254
179
 
255
- ## API Reference
180
+ ```typescript
181
+ import { SecureKentuckySignerClient, EphemeralKeyManager } from 'kentucky-signer-viem'
256
182
 
257
- ### Core Functions
183
+ // Create ephemeral key manager
184
+ const keyManager = new EphemeralKeyManager()
258
185
 
259
- #### `createKentuckySignerAccount(options)`
186
+ // Create secure client
187
+ const secureClient = new SecureKentuckySignerClient({
188
+ baseUrl: 'https://signer.example.com',
189
+ ephemeralKeyManager: keyManager,
190
+ })
260
191
 
261
- Creates a custom Viem account backed by Kentucky Signer.
192
+ // Authenticate with ephemeral key binding
193
+ const session = await authenticateWithPasskey({
194
+ baseUrl: 'https://signer.example.com',
195
+ accountId: 'account_id',
196
+ ephemeralPublicKey: await keyManager.getPublicKey(),
197
+ })
262
198
 
263
- ```typescript
199
+ // Create account with secure client
264
200
  const account = createKentuckySignerAccount({
265
- config: {
266
- baseUrl: string, // Kentucky Signer API URL
267
- accountId: string, // 64-char hex account ID
268
- },
269
- session: AuthSession, // Authenticated session
270
- defaultChainId?: number, // Default chain ID (default: 1)
271
- onSessionExpired?: () => Promise<AuthSession>, // Session refresh callback
201
+ config: { baseUrl, accountId },
202
+ session,
203
+ secureClient, // Uses ephemeral key signing
272
204
  })
273
205
  ```
274
206
 
275
- #### `authenticateWithPasskey(options)`
207
+ ## Two-Factor Authentication
276
208
 
277
- Authenticates using WebAuthn passkey (browser only).
209
+ ### Setup TOTP (Authenticator App)
278
210
 
279
211
  ```typescript
280
- const session = await authenticateWithPasskey({
281
- baseUrl: string, // Kentucky Signer API URL
282
- accountId: string, // Account ID to authenticate
283
- rpId?: string, // WebAuthn Relying Party ID
284
- allowCredentials?: string[], // Allowed credential IDs
285
- })
286
- ```
212
+ import { KentuckySignerClient } from 'kentucky-signer-viem'
287
213
 
288
- #### `authenticateWithPassword(options)`
214
+ const client = new KentuckySignerClient({ baseUrl })
289
215
 
290
- Authenticates using password (works in browser and Node.js).
216
+ // Start TOTP setup - returns QR code URI
217
+ const setup = await client.setupTOTP(token)
218
+ console.log('Scan this QR code:', setup.uri)
219
+ console.log('Or enter manually:', setup.secret)
291
220
 
292
- ```typescript
293
- const session = await authenticateWithPassword({
294
- baseUrl: string, // Kentucky Signer API URL
295
- accountId: string, // Account ID to authenticate
296
- password: string, // Account password
297
- })
298
- ```
221
+ // Enable TOTP with verification code
222
+ await client.enableTOTP('123456', token)
299
223
 
300
- #### `createAccountWithPassword(options)`
224
+ // Check 2FA status
225
+ const status = await client.get2FAStatus(token)
226
+ // { totp_enabled: true, pin_enabled: false, pin_length: 0 }
227
+ ```
301
228
 
302
- Creates a new account with password authentication.
229
+ ### Setup PIN
303
230
 
304
231
  ```typescript
305
- const account = await createAccountWithPassword({
306
- baseUrl: string, // Kentucky Signer API URL
307
- password: string, // Password (8-128 characters)
308
- confirmation: string, // Must match password
309
- })
310
- // Returns: { account_id, addresses: { evm, bitcoin, solana } }
232
+ // Setup 4 or 6 digit PIN
233
+ await client.setupPIN('123456', token)
234
+
235
+ // Disable PIN (requires current PIN)
236
+ await client.disablePIN('123456', token)
311
237
  ```
312
238
 
313
- #### `authenticateWithToken(baseUrl, accountId, token, expiresAt?)`
239
+ ### Signing with 2FA
314
240
 
315
- Creates a session from an existing JWT token (Node.js compatible).
241
+ When 2FA is enabled, signing operations will automatically prompt for codes:
316
242
 
317
- ```typescript
318
- const session = await authenticateWithToken(
319
- 'https://signer.example.com',
320
- 'account_id',
321
- 'jwt_token',
322
- Date.now() + 3600000 // Optional expiration
323
- )
324
- ```
243
+ ```tsx
244
+ import { useKentuckySigner } from 'kentucky-signer-viem/react'
325
245
 
326
- ### Client Class
246
+ function App() {
247
+ const { twoFactorPrompt, submit2FA, cancel2FA } = useKentuckySigner()
248
+
249
+ // The 2FA prompt appears automatically when signing requires it
250
+ if (twoFactorPrompt.isVisible) {
251
+ return (
252
+ <TwoFactorModal
253
+ totpRequired={twoFactorPrompt.totpRequired}
254
+ pinRequired={twoFactorPrompt.pinRequired}
255
+ pinLength={twoFactorPrompt.pinLength}
256
+ onSubmit={(codes) => submit2FA(codes)}
257
+ onCancel={() => cancel2FA()}
258
+ />
259
+ )
260
+ }
261
+
262
+ return <YourApp />
263
+ }
264
+ ```
327
265
 
328
- #### `KentuckySignerClient`
266
+ ## Guardian Recovery
329
267
 
330
- Low-level API client for Kentucky Signer.
268
+ Set up trusted guardians for account recovery:
331
269
 
332
270
  ```typescript
333
- const client = new KentuckySignerClient({ baseUrl: 'https://signer.example.com' })
271
+ const client = new KentuckySignerClient({ baseUrl })
272
+
273
+ // Add a guardian (requires WebAuthn attestation from guardian's device)
274
+ const { guardian_index, guardian_count } = await client.addGuardian({
275
+ attestation_object: guardianAttestationBase64url,
276
+ label: 'My Friend',
277
+ }, token)
278
+
279
+ // List guardians
280
+ const { guardians } = await client.getGuardians(token)
281
+ // guardians: [{ index: 1, label: 'My Friend' }, ...]
282
+
283
+ // Initiate recovery (when locked out - register new passkey first)
284
+ const recovery = await client.initiateRecovery(
285
+ accountId,
286
+ newPasskeyAttestationObject,
287
+ 'New Owner Passkey'
288
+ )
289
+ // Returns: { challenges, guardian_count, threshold, timelock_seconds }
290
+
291
+ // Guardian signs their challenge with their passkey
292
+ await client.verifyGuardian({
293
+ account_id: accountId,
294
+ guardian_index: 1,
295
+ authenticator_data: authDataBase64url,
296
+ client_data_json: clientDataBase64url,
297
+ signature: signatureBase64url,
298
+ })
334
299
 
335
- // Get challenge for authentication
336
- const challenge = await client.getChallenge(accountId)
300
+ // Check recovery status
301
+ const status = await client.getRecoveryStatus(accountId)
302
+ // { verified_count, threshold, can_complete, timelock_remaining }
337
303
 
338
- // Authenticate with passkey credential
339
- const auth = await client.authenticatePasskey(accountId, credential)
304
+ // Complete recovery after threshold met and timelock expired
305
+ await client.completeRecovery(accountId)
306
+ ```
340
307
 
341
- // Sign EVM transaction
342
- const signature = await client.signEvmTransaction(
343
- { tx_hash: '0x...', chain_id: 1 },
344
- jwtToken
345
- )
308
+ ## API Reference
346
309
 
347
- // Get account info
348
- const info = await client.getAccountInfo(accountId, jwtToken)
349
- ```
310
+ ### Core Functions
311
+
312
+ | Function | Description |
313
+ |----------|-------------|
314
+ | `createKentuckySignerAccount(options)` | Create a Viem-compatible account |
315
+ | `createServerAccount(...)` | Create account with JWT token (Node.js) |
316
+ | `authenticateWithPasskey(options)` | Authenticate using WebAuthn |
317
+ | `authenticateWithPassword(options)` | Authenticate using password |
318
+ | `createAccountWithPassword(options)` | Create new account with password |
319
+ | `authenticateWithToken(...)` | Create session from JWT token |
350
320
 
351
321
  ### React Hooks
352
322
 
353
323
  | Hook | Description |
354
324
  |------|-------------|
355
- | `useKentuckySigner()` | Access auth state and actions |
325
+ | `useKentuckySigner()` | Access auth state, actions, and 2FA |
356
326
  | `useKentuckySignerAccount()` | Get the current account |
357
327
  | `useWalletClient(options)` | Create a Viem WalletClient |
358
328
  | `usePasskeyAuth()` | Authentication flow with loading state |
@@ -361,42 +331,43 @@ const info = await client.getAccountInfo(accountId, jwtToken)
361
331
  | `useIsReady()` | Check if signer is ready |
362
332
  | `useAddress()` | Get connected address |
363
333
 
364
- ### Types
365
-
366
- ```typescript
367
- interface AuthSession {
368
- token: string // JWT access token
369
- accountId: string // Account ID
370
- evmAddress: Address // EVM address
371
- btcAddress?: string // Bitcoin address
372
- solAddress?: string // Solana address
373
- expiresAt: number // Expiration timestamp (ms)
374
- }
375
-
376
- interface KentuckySignerConfig {
377
- baseUrl: string // API URL
378
- accountId: string // Account ID
379
- }
380
- ```
381
-
382
- ## Session Management
383
-
384
- Sessions are automatically managed:
385
-
386
- - **Browser**: Sessions can be persisted to localStorage
387
- - **Auto-refresh**: Sessions are refreshed before expiration
388
- - **Expiration handling**: Provide `onSessionExpired` callback for custom handling
389
-
390
- ```typescript
391
- const account = createKentuckySignerAccount({
392
- config: { baseUrl, accountId },
393
- session,
394
- onSessionExpired: async () => {
395
- // Re-authenticate or refresh token
396
- return await authenticateWithPasskey({ baseUrl, accountId })
397
- },
398
- })
399
- ```
334
+ ### Client Methods
335
+
336
+ #### Authentication
337
+ - `getChallenge(accountId)` - Get WebAuthn challenge
338
+ - `authenticatePasskey(accountId, credential, ephemeralPublicKey?)` - Authenticate with passkey
339
+ - `authenticatePassword(request)` - Authenticate with password (`{ account_id, password }`)
340
+ - `refreshToken(token)` - Refresh JWT token
341
+ - `logout(token)` - Invalidate session
342
+
343
+ #### Signing
344
+ - `signEvmTransaction(request, token)` - Sign EVM transaction hash
345
+ - `signEvmTransactionWith2FA(request, token)` - Sign with 2FA codes
346
+
347
+ #### Account Management
348
+ - `getAccountInfo(accountId, token)` - Get account info
349
+ - `getAccountInfoExtended(accountId, token)` - Get account info with auth config
350
+ - `addPassword(accountId, request, token)` - Add password auth
351
+ - `addPasskey(accountId, request, token)` - Add passkey
352
+ - `removePasskey(accountId, passkeyIndex, token)` - Remove passkey by index
353
+
354
+ #### Two-Factor Authentication
355
+ - `get2FAStatus(token)` - Get 2FA status
356
+ - `setupTOTP(token)` - Start TOTP setup
357
+ - `enableTOTP(code, token)` - Enable TOTP
358
+ - `disableTOTP(code, token)` - Disable TOTP
359
+ - `setupPIN(pin, token)` - Setup PIN
360
+ - `disablePIN(pin, token)` - Disable PIN
361
+
362
+ #### Guardian Recovery
363
+ - `addGuardian(request, token)` - Add guardian passkey
364
+ - `removeGuardian(guardianIndex, token)` - Remove guardian
365
+ - `getGuardians(token)` - List guardians
366
+ - `initiateRecovery(accountId, attestationObject, label?)` - Start recovery
367
+ - `verifyGuardian(request)` - Submit guardian signature
368
+ - `getRecoveryStatus(accountId)` - Check recovery status
369
+ - `completeRecovery(accountId)` - Complete recovery
370
+ - `cancelRecovery(token)` - Cancel recovery (owner only)
400
371
 
401
372
  ## Error Handling
402
373
 
@@ -407,21 +378,38 @@ try {
407
378
  await authenticate(accountId)
408
379
  } catch (error) {
409
380
  if (error instanceof KentuckySignerError) {
410
- console.error('Code:', error.code)
411
- console.error('Message:', error.message)
412
- console.error('Details:', error.details)
381
+ switch (error.code) {
382
+ case 'WEBAUTHN_NOT_AVAILABLE':
383
+ // WebAuthn not supported
384
+ break
385
+ case 'USER_CANCELLED':
386
+ // User cancelled authentication
387
+ break
388
+ case 'SESSION_EXPIRED':
389
+ // JWT token expired
390
+ break
391
+ case '2FA_REQUIRED':
392
+ // 2FA verification needed
393
+ break
394
+ case '2FA_CANCELLED':
395
+ // User cancelled 2FA input
396
+ break
397
+ // ... handle other codes
398
+ }
413
399
  }
414
400
  }
415
401
  ```
416
402
 
417
- Common error codes:
418
- - `WEBAUTHN_NOT_AVAILABLE` - WebAuthn not supported
419
- - `USER_CANCELLED` - User cancelled authentication
420
- - `SESSION_EXPIRED` - JWT token expired
421
- - `UNAUTHORIZED` - Invalid or missing token
422
- - `NOT_FOUND` - Account not found
423
- - `PASSWORD_MISMATCH` - Password and confirmation don't match
424
- - `INVALID_PASSWORD` - Password doesn't meet requirements (8-128 chars)
403
+ ## Documentation
404
+
405
+ For detailed documentation, see the [docs](./docs) folder:
406
+
407
+ - [Authentication](./docs/authentication.md) - Passkey, password, and token auth
408
+ - [Secure Mode](./docs/secure-mode.md) - Ephemeral key signing
409
+ - [Two-Factor Authentication](./docs/two-factor-auth.md) - TOTP and PIN setup
410
+ - [Guardian Recovery](./docs/guardian-recovery.md) - Social recovery setup
411
+ - [React Integration](./docs/react-integration.md) - Hooks and context usage
412
+ - [API Reference](./docs/api-reference.md) - Complete API documentation
425
413
 
426
414
  ## License
427
415