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 +207 -219
- package/dist/index.d.mts +802 -7
- package/dist/index.d.ts +802 -7
- package/dist/index.js +964 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +955 -37
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +61 -3
- package/dist/react/index.d.ts +61 -3
- package/dist/react/index.js +1286 -173
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1288 -174
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/account.ts +111 -22
- package/src/auth.ts +16 -6
- package/src/client.ts +438 -18
- package/src/ephemeral.ts +407 -0
- package/src/index.ts +56 -0
- package/src/react/context.tsx +360 -45
- package/src/react/hooks.ts +11 -0
- package/src/react/index.ts +1 -0
- package/src/secure-client.ts +417 -0
- package/src/types.ts +332 -0
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
*
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
361
|
-
const
|
|
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(
|