kentucky-signer-viem 0.1.1 → 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/src/account.ts CHANGED
@@ -19,6 +19,28 @@ import {
19
19
  import { toAccount } from 'viem/accounts'
20
20
  import type { AuthSession, KentuckySignerConfig } from './types'
21
21
  import { KentuckySignerClient, KentuckySignerError } from './client'
22
+ import { SecureKentuckySignerClient } from './secure-client'
23
+ import type { EphemeralKeyManager } from './ephemeral'
24
+
25
+ /**
26
+ * 2FA codes for signing operations
27
+ */
28
+ export interface TwoFactorCodes {
29
+ /** TOTP code from authenticator app */
30
+ totpCode?: string
31
+ /** PIN code */
32
+ pin?: string
33
+ }
34
+
35
+ /**
36
+ * Callback to request 2FA codes from the user
37
+ * Returns null/undefined if user cancels
38
+ */
39
+ export type TwoFactorCallback = (requirements: {
40
+ totpRequired: boolean
41
+ pinRequired: boolean
42
+ pinLength: number
43
+ }) => Promise<TwoFactorCodes | null | undefined>
22
44
 
23
45
  /**
24
46
  * Options for creating a Kentucky Signer account
@@ -32,6 +54,10 @@ export interface KentuckySignerAccountOptions {
32
54
  defaultChainId?: number
33
55
  /** Callback when session needs refresh */
34
56
  onSessionExpired?: () => Promise<AuthSession>
57
+ /** Optional secure client for ephemeral key signing */
58
+ secureClient?: SecureKentuckySignerClient
59
+ /** Callback to request 2FA codes when required */
60
+ on2FARequired?: TwoFactorCallback
35
61
  }
36
62
 
37
63
  /**
@@ -81,10 +107,11 @@ export interface KentuckySignerAccount extends LocalAccount<'kentuckySigner'> {
81
107
  export function createKentuckySignerAccount(
82
108
  options: KentuckySignerAccountOptions
83
109
  ): KentuckySignerAccount {
84
- const { config, defaultChainId = 1, onSessionExpired } = options
110
+ const { config, defaultChainId = 1, onSessionExpired, secureClient, on2FARequired } = options
85
111
  let session = options.session
86
112
 
87
- const client = new KentuckySignerClient({ baseUrl: config.baseUrl })
113
+ // Use secure client if provided, otherwise use standard client
114
+ const client = secureClient ?? new KentuckySignerClient({ baseUrl: config.baseUrl })
88
115
 
89
116
  /**
90
117
  * Get current token, refreshing if needed
@@ -106,26 +133,91 @@ export function createKentuckySignerAccount(
106
133
  }
107
134
 
108
135
  /**
109
- * Sign a hash using Kentucky Signer
136
+ * Sign a hash using Kentucky Signer and return full signature
137
+ * Handles 2FA by detecting the error and calling the callback
110
138
  */
111
139
  async function signHash(hash: Hex, chainId: number): Promise<Hex> {
112
140
  const token = await getToken()
113
- const response = await client.signEvmTransaction(
114
- { tx_hash: hash, chain_id: chainId },
115
- token
116
- )
117
- return response.signature.full
141
+
142
+ // First attempt without 2FA codes
143
+ try {
144
+ const response = await client.signEvmTransaction(
145
+ { tx_hash: hash, chain_id: chainId },
146
+ token
147
+ )
148
+ return response.signature.full
149
+ } catch (err) {
150
+ // Check if 2FA is required
151
+ if (err instanceof KentuckySignerError && err.code === '2FA_REQUIRED' && on2FARequired) {
152
+ // Parse requirements from error details
153
+ const totpRequired = err.message.includes('TOTP') || (err.details?.includes('totp_code') ?? false)
154
+ const pinRequired = err.message.includes('PIN') || (err.details?.includes('pin') ?? false)
155
+ // Default to 6-digit PIN if required
156
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)![1]) : 6
157
+
158
+ // Request 2FA codes from user
159
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength })
160
+ if (!codes) {
161
+ throw new KentuckySignerError('2FA verification cancelled', '2FA_CANCELLED', 'User cancelled 2FA input')
162
+ }
163
+
164
+ // Retry with 2FA codes
165
+ const response = await client.signEvmTransactionWith2FA(
166
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
167
+ token
168
+ )
169
+ return response.signature.full
170
+ }
171
+ throw err
172
+ }
118
173
  }
119
174
 
120
175
  /**
121
- * Parse signature components from full signature
176
+ * Sign a hash using Kentucky Signer and return signature components
177
+ * Handles 2FA by detecting the error and calling the callback
122
178
  */
123
- function parseSignature(signature: Hex): { r: Hex; s: Hex; v: bigint } {
124
- // Signature is 65 bytes: r (32) + s (32) + v (1)
125
- const r = `0x${signature.slice(2, 66)}` as Hex
126
- const s = `0x${signature.slice(66, 130)}` as Hex
127
- const v = BigInt(`0x${signature.slice(130, 132)}`)
128
- return { r, s, v }
179
+ async function signHashWithComponents(hash: Hex, chainId: number): Promise<{ r: Hex; s: Hex; v: number }> {
180
+ const token = await getToken()
181
+
182
+ // First attempt without 2FA codes
183
+ try {
184
+ const response = await client.signEvmTransaction(
185
+ { tx_hash: hash, chain_id: chainId },
186
+ token
187
+ )
188
+ return {
189
+ r: response.signature.r,
190
+ s: response.signature.s,
191
+ v: response.signature.v,
192
+ }
193
+ } catch (err) {
194
+ // Check if 2FA is required
195
+ if (err instanceof KentuckySignerError && err.code === '2FA_REQUIRED' && on2FARequired) {
196
+ // Parse requirements from error details
197
+ const totpRequired = err.message.includes('TOTP') || (err.details?.includes('totp_code') ?? false)
198
+ const pinRequired = err.message.includes('PIN') || (err.details?.includes('pin') ?? false)
199
+ // Default to 6-digit PIN if required
200
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)![1]) : 6
201
+
202
+ // Request 2FA codes from user
203
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength })
204
+ if (!codes) {
205
+ throw new KentuckySignerError('2FA verification cancelled', '2FA_CANCELLED', 'User cancelled 2FA input')
206
+ }
207
+
208
+ // Retry with 2FA codes
209
+ const response = await client.signEvmTransactionWith2FA(
210
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
211
+ token
212
+ )
213
+ return {
214
+ r: response.signature.r,
215
+ s: response.signature.s,
216
+ v: response.signature.v,
217
+ }
218
+ }
219
+ throw err
220
+ }
129
221
  }
130
222
 
131
223
  const account = toAccount({
@@ -159,11 +251,8 @@ export function createKentuckySignerAccount(
159
251
  // Hash the serialized transaction
160
252
  const txHash = keccak256(serializedUnsigned)
161
253
 
162
- // Sign the hash
163
- const signature = await signHash(txHash, chainId)
164
-
165
- // Parse signature components
166
- const { r, s, v } = parseSignature(signature)
254
+ // Sign the hash and get components directly from API
255
+ const { r, s, v } = await signHashWithComponents(txHash, chainId)
167
256
 
168
257
  // For EIP-1559 and EIP-2930 transactions, v is 0 or 1
169
258
  // For legacy transactions, v is chainId * 2 + 35 + recovery
@@ -174,10 +263,10 @@ export function createKentuckySignerAccount(
174
263
  transaction.type === 'eip4844' ||
175
264
  transaction.type === 'eip7702'
176
265
  ) {
177
- yParity = Number(v) - 27 // Convert from 27/28 to 0/1
266
+ yParity = v >= 27 ? v - 27 : v // Convert from 27/28 to 0/1 if needed
178
267
  } else {
179
268
  // Legacy transaction - v already includes chain ID
180
- yParity = Number(v)
269
+ yParity = v
181
270
  }
182
271
 
183
272
  // Serialize with signature
package/src/auth.ts CHANGED
@@ -164,7 +164,8 @@ export async function authenticateWithPasskey(
164
164
  const passkeyCredential = credentialToPasskey(credential)
165
165
  const authResponse = await client.authenticatePasskey(
166
166
  options.accountId,
167
- passkeyCredential
167
+ passkeyCredential,
168
+ options.ephemeralPublicKey
168
169
  )
169
170
 
170
171
  // Step 5: Get account info to retrieve addresses
@@ -222,11 +223,11 @@ export async function authenticateWithToken(
222
223
  * Register a new passkey for account creation (browser only)
223
224
  *
224
225
  * @param options - Registration options
225
- * @returns Registration credential with public key
226
+ * @returns Registration credential with public key and attestation object
226
227
  */
227
228
  export async function registerPasskey(
228
229
  options: PasskeyRegistrationOptions
229
- ): Promise<PasskeyCredential & { publicKey: string }> {
230
+ ): Promise<PasskeyCredential & { publicKey: string; attestationObject: string }> {
230
231
  if (!isWebAuthnAvailable()) {
231
232
  throw new KentuckySignerError(
232
233
  'WebAuthn is not available in this environment',
@@ -294,6 +295,7 @@ export async function registerPasskey(
294
295
  credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
295
296
  clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
296
297
  authenticatorData: base64UrlEncode(new Uint8Array(response.getAuthenticatorData())),
298
+ attestationObject: base64UrlEncode(new Uint8Array(response.attestationObject)),
297
299
  signature: '', // Not applicable for registration
298
300
  publicKey: base64UrlEncode(new Uint8Array(publicKeyBytes)),
299
301
  }
@@ -357,11 +359,19 @@ export async function authenticateWithPassword(
357
359
  ): Promise<AuthSession> {
358
360
  const client = new KentuckySignerClient({ baseUrl: options.baseUrl })
359
361
 
360
- // Authenticate with password
361
- const authResponse = await client.authenticatePassword({
362
+ // Build auth request with optional ephemeral key
363
+ const authRequest: { account_id: string; password: string; ephemeral_public_key?: string } = {
362
364
  account_id: options.accountId,
363
365
  password: options.password,
364
- })
366
+ }
367
+
368
+ // Add ephemeral public key if provided (for secure mode binding)
369
+ if (options.ephemeralPublicKey) {
370
+ authRequest.ephemeral_public_key = options.ephemeralPublicKey
371
+ }
372
+
373
+ // Authenticate with password
374
+ const authResponse = await client.authenticatePassword(authRequest)
365
375
 
366
376
  // Get account info to retrieve addresses
367
377
  const accountInfo = await client.getAccountInfo(