kentucky-signer-viem 0.1.0 → 0.1.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/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,166 @@ 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
176
+ ## Secure Mode (Ephemeral Keys)
231
177
 
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
- }
243
-
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'
245
+
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
+ }
325
261
 
326
- ### Client Class
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 })
334
272
 
335
- // Get challenge for authentication
336
- const challenge = await client.getChallenge(accountId)
273
+ // Add a guardian
274
+ await client.addGuardian(accountId, {
275
+ guardian_account_id: 'guardian_hex_id',
276
+ label: 'My Friend',
277
+ }, token)
337
278
 
338
- // Authenticate with passkey credential
339
- const auth = await client.authenticatePasskey(accountId, credential)
279
+ // List guardians
280
+ const { guardians } = await client.getGuardians(accountId, token)
340
281
 
341
- // Sign EVM transaction
342
- const signature = await client.signEvmTransaction(
343
- { tx_hash: '0x...', chain_id: 1 },
344
- jwtToken
345
- )
282
+ // Initiate recovery (when locked out)
283
+ const recovery = await client.initiateRecovery({
284
+ account_id: accountId,
285
+ new_password: 'new-secure-password',
286
+ })
346
287
 
347
- // Get account info
348
- const info = await client.getAccountInfo(accountId, jwtToken)
288
+ // Guardian approves recovery
289
+ await client.verifyGuardianRecovery({
290
+ recovery_id: recovery.recovery_id,
291
+ guardian_account_id: guardianId,
292
+ signature: guardianSignature,
293
+ }, guardianToken)
294
+
295
+ // Complete recovery after threshold met
296
+ await client.completeRecovery({
297
+ recovery_id: recovery.recovery_id,
298
+ }, token)
349
299
  ```
350
300
 
301
+ ## API Reference
302
+
303
+ ### Core Functions
304
+
305
+ | Function | Description |
306
+ |----------|-------------|
307
+ | `createKentuckySignerAccount(options)` | Create a Viem-compatible account |
308
+ | `createServerAccount(...)` | Create account with JWT token (Node.js) |
309
+ | `authenticateWithPasskey(options)` | Authenticate using WebAuthn |
310
+ | `authenticateWithPassword(options)` | Authenticate using password |
311
+ | `createAccountWithPassword(options)` | Create new account with password |
312
+ | `authenticateWithToken(...)` | Create session from JWT token |
313
+
351
314
  ### React Hooks
352
315
 
353
316
  | Hook | Description |
354
317
  |------|-------------|
355
- | `useKentuckySigner()` | Access auth state and actions |
318
+ | `useKentuckySigner()` | Access auth state, actions, and 2FA |
356
319
  | `useKentuckySignerAccount()` | Get the current account |
357
320
  | `useWalletClient(options)` | Create a Viem WalletClient |
358
321
  | `usePasskeyAuth()` | Authentication flow with loading state |
@@ -361,42 +324,39 @@ const info = await client.getAccountInfo(accountId, jwtToken)
361
324
  | `useIsReady()` | Check if signer is ready |
362
325
  | `useAddress()` | Get connected address |
363
326
 
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
- ```
327
+ ### Client Methods
328
+
329
+ #### Authentication
330
+ - `getChallenge(accountId)` - Get WebAuthn challenge
331
+ - `authenticatePasskey(accountId, credential)` - Authenticate with passkey
332
+ - `authenticatePassword(accountId, password)` - Authenticate with password
333
+ - `logout(token)` - Invalidate session
334
+
335
+ #### Signing
336
+ - `signEvmTransaction(request, token)` - Sign EVM transaction hash
337
+ - `signEvmTransactionWith2FA(request, token)` - Sign with 2FA codes
338
+
339
+ #### Account Management
340
+ - `getAccountInfo(accountId, token)` - Get account info
341
+ - `addPassword(accountId, request, token)` - Add password auth
342
+ - `addPasskey(accountId, request, token)` - Add passkey
343
+ - `removePasskey(accountId, credentialId, token)` - Remove passkey
344
+
345
+ #### Two-Factor Authentication
346
+ - `get2FAStatus(token)` - Get 2FA status
347
+ - `setupTOTP(token)` - Start TOTP setup
348
+ - `enableTOTP(code, token)` - Enable TOTP
349
+ - `disableTOTP(code, token)` - Disable TOTP
350
+ - `setupPIN(pin, token)` - Setup PIN
351
+ - `disablePIN(pin, token)` - Disable PIN
352
+
353
+ #### Guardian Recovery
354
+ - `addGuardian(accountId, request, token)` - Add guardian
355
+ - `removeGuardian(accountId, guardianId, token)` - Remove guardian
356
+ - `getGuardians(accountId, token)` - List guardians
357
+ - `initiateRecovery(request)` - Start recovery
358
+ - `verifyGuardianRecovery(request, token)` - Guardian approval
359
+ - `completeRecovery(request, token)` - Complete recovery
400
360
 
401
361
  ## Error Handling
402
362
 
@@ -407,21 +367,38 @@ try {
407
367
  await authenticate(accountId)
408
368
  } catch (error) {
409
369
  if (error instanceof KentuckySignerError) {
410
- console.error('Code:', error.code)
411
- console.error('Message:', error.message)
412
- console.error('Details:', error.details)
370
+ switch (error.code) {
371
+ case 'WEBAUTHN_NOT_AVAILABLE':
372
+ // WebAuthn not supported
373
+ break
374
+ case 'USER_CANCELLED':
375
+ // User cancelled authentication
376
+ break
377
+ case 'SESSION_EXPIRED':
378
+ // JWT token expired
379
+ break
380
+ case '2FA_REQUIRED':
381
+ // 2FA verification needed
382
+ break
383
+ case '2FA_CANCELLED':
384
+ // User cancelled 2FA input
385
+ break
386
+ // ... handle other codes
387
+ }
413
388
  }
414
389
  }
415
390
  ```
416
391
 
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)
392
+ ## Documentation
393
+
394
+ For detailed documentation, see the [docs](./docs) folder:
395
+
396
+ - [Authentication](./docs/authentication.md) - Passkey, password, and token auth
397
+ - [Secure Mode](./docs/secure-mode.md) - Ephemeral key signing
398
+ - [Two-Factor Authentication](./docs/two-factor-auth.md) - TOTP and PIN setup
399
+ - [Guardian Recovery](./docs/guardian-recovery.md) - Social recovery setup
400
+ - [React Integration](./docs/react-integration.md) - Hooks and context usage
401
+ - [API Reference](./docs/api-reference.md) - Complete API documentation
425
402
 
426
403
  ## License
427
404