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 +195 -218
- 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/react/context.tsx
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
2
|
createContext,
|
|
3
3
|
useContext,
|
|
4
4
|
useState,
|
|
5
5
|
useCallback,
|
|
6
6
|
useEffect,
|
|
7
7
|
useMemo,
|
|
8
|
+
useRef,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
} from 'react'
|
|
10
11
|
import type { AuthSession } from '../types'
|
|
12
|
+
import type { TwoFactorCodes } from '../account'
|
|
11
13
|
import { KentuckySignerClient } from '../client'
|
|
14
|
+
import { SecureKentuckySignerClient } from '../secure-client'
|
|
12
15
|
import {
|
|
13
16
|
authenticateWithPasskey,
|
|
17
|
+
authenticateWithPassword as authWithPassword,
|
|
14
18
|
isSessionValid,
|
|
15
19
|
refreshSessionIfNeeded,
|
|
16
20
|
LocalStorageTokenStorage,
|
|
@@ -19,6 +23,27 @@ import {
|
|
|
19
23
|
createKentuckySignerAccount,
|
|
20
24
|
type KentuckySignerAccount,
|
|
21
25
|
} from '../account'
|
|
26
|
+
import {
|
|
27
|
+
EphemeralKeyManager,
|
|
28
|
+
IndexedDBEphemeralKeyStorage,
|
|
29
|
+
MemoryEphemeralKeyStorage,
|
|
30
|
+
} from '../ephemeral'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 2FA prompt requirements
|
|
34
|
+
*/
|
|
35
|
+
export interface TwoFactorPromptState {
|
|
36
|
+
/** Whether the 2FA prompt is visible */
|
|
37
|
+
isVisible: boolean
|
|
38
|
+
/** Whether TOTP is required */
|
|
39
|
+
totpRequired: boolean
|
|
40
|
+
/** Whether PIN is required */
|
|
41
|
+
pinRequired: boolean
|
|
42
|
+
/** Expected PIN length (4 or 6) */
|
|
43
|
+
pinLength: number
|
|
44
|
+
/** Callback to resolve with codes */
|
|
45
|
+
resolve?: (codes: TwoFactorCodes | null) => void
|
|
46
|
+
}
|
|
22
47
|
|
|
23
48
|
/**
|
|
24
49
|
* Kentucky Signer context state
|
|
@@ -34,20 +59,40 @@ export interface KentuckySignerState {
|
|
|
34
59
|
account: KentuckySignerAccount | null
|
|
35
60
|
/** Last error (if any) */
|
|
36
61
|
error: Error | null
|
|
62
|
+
/** Whether ephemeral key is bound to the token */
|
|
63
|
+
ephemeralKeyBound: boolean
|
|
64
|
+
/** Whether secure mode (ephemeral key signing) is enabled */
|
|
65
|
+
secureMode: boolean
|
|
66
|
+
/** Whether ephemeral keys are persisted in IndexedDB (survives refresh) */
|
|
67
|
+
persistEphemeralKeys: boolean
|
|
68
|
+
/** 2FA prompt state */
|
|
69
|
+
twoFactorPrompt: TwoFactorPromptState
|
|
37
70
|
}
|
|
38
71
|
|
|
39
72
|
/**
|
|
40
73
|
* Kentucky Signer context actions
|
|
41
74
|
*/
|
|
42
75
|
export interface KentuckySignerActions {
|
|
43
|
-
/** Authenticate with passkey */
|
|
44
|
-
authenticate: (accountId: string, options?: { rpId?: string }) => Promise<void>
|
|
76
|
+
/** Authenticate with passkey or set an existing session */
|
|
77
|
+
authenticate: (accountId: string, options?: { rpId?: string; session?: AuthSession }) => Promise<void>
|
|
78
|
+
/** Authenticate with password */
|
|
79
|
+
authenticatePassword: (accountId: string, password: string) => Promise<void>
|
|
45
80
|
/** Logout and clear session */
|
|
46
81
|
logout: () => Promise<void>
|
|
47
82
|
/** Refresh session if needed */
|
|
48
83
|
refreshSession: () => Promise<void>
|
|
49
84
|
/** Clear any errors */
|
|
50
85
|
clearError: () => void
|
|
86
|
+
/** Toggle secure mode (ephemeral key signing) */
|
|
87
|
+
setSecureMode: (enabled: boolean) => void
|
|
88
|
+
/** Toggle ephemeral key persistence (IndexedDB vs memory) */
|
|
89
|
+
setPersistEphemeralKeys: (enabled: boolean) => void
|
|
90
|
+
/** Get the ephemeral public key (for secure mode binding during external auth) */
|
|
91
|
+
getEphemeralPublicKey: () => Promise<string | undefined>
|
|
92
|
+
/** Submit 2FA codes (called by 2FA prompt UI) */
|
|
93
|
+
submit2FA: (codes: TwoFactorCodes) => void
|
|
94
|
+
/** Cancel 2FA prompt */
|
|
95
|
+
cancel2FA: () => void
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
/**
|
|
@@ -69,6 +114,8 @@ export interface KentuckySignerProviderProps {
|
|
|
69
114
|
storageKeyPrefix?: string
|
|
70
115
|
/** Whether to persist session in localStorage */
|
|
71
116
|
persistSession?: boolean
|
|
117
|
+
/** Whether to use ephemeral key signing for enhanced security */
|
|
118
|
+
useEphemeralKeys?: boolean
|
|
72
119
|
/** Children */
|
|
73
120
|
children: ReactNode
|
|
74
121
|
}
|
|
@@ -90,33 +137,119 @@ export function KentuckySignerProvider({
|
|
|
90
137
|
defaultChainId = 1,
|
|
91
138
|
storageKeyPrefix = 'kentucky_signer',
|
|
92
139
|
persistSession = true,
|
|
140
|
+
useEphemeralKeys = false,
|
|
93
141
|
children,
|
|
94
142
|
}: KentuckySignerProviderProps) {
|
|
143
|
+
// Load persist ephemeral keys setting from localStorage, default to IndexedDB if available
|
|
144
|
+
const getInitialPersistSetting = (): boolean => {
|
|
145
|
+
if (typeof localStorage !== 'undefined') {
|
|
146
|
+
const saved = localStorage.getItem(`${storageKeyPrefix}_persist_ephemeral_keys`)
|
|
147
|
+
if (saved !== null) {
|
|
148
|
+
return saved === 'true'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Default to IndexedDB persistence if available
|
|
152
|
+
return typeof indexedDB !== 'undefined'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Load secure mode setting from localStorage, falling back to useEphemeralKeys prop
|
|
156
|
+
const getInitialSecureModeSetting = (): boolean => {
|
|
157
|
+
if (typeof localStorage !== 'undefined') {
|
|
158
|
+
const saved = localStorage.getItem(`${storageKeyPrefix}_secure_mode`)
|
|
159
|
+
if (saved !== null) {
|
|
160
|
+
return saved === 'true'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Fall back to prop value
|
|
164
|
+
return useEphemeralKeys
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Default 2FA prompt state
|
|
168
|
+
const defaultTwoFactorPrompt: TwoFactorPromptState = {
|
|
169
|
+
isVisible: false,
|
|
170
|
+
totpRequired: false,
|
|
171
|
+
pinRequired: false,
|
|
172
|
+
pinLength: 6,
|
|
173
|
+
}
|
|
174
|
+
|
|
95
175
|
const [state, setState] = useState<KentuckySignerState>({
|
|
96
176
|
isAuthenticating: false,
|
|
97
177
|
isAuthenticated: false,
|
|
98
178
|
session: null,
|
|
99
179
|
account: null,
|
|
100
180
|
error: null,
|
|
181
|
+
ephemeralKeyBound: false,
|
|
182
|
+
secureMode: getInitialSecureModeSetting(),
|
|
183
|
+
persistEphemeralKeys: getInitialPersistSetting(),
|
|
184
|
+
twoFactorPrompt: defaultTwoFactorPrompt,
|
|
101
185
|
})
|
|
102
186
|
|
|
187
|
+
// Ephemeral key storage instances - we keep both and switch between them
|
|
188
|
+
const indexedDBStorage = useMemo(
|
|
189
|
+
() => typeof indexedDB !== 'undefined' ? new IndexedDBEphemeralKeyStorage() : null,
|
|
190
|
+
[]
|
|
191
|
+
)
|
|
192
|
+
const memoryStorage = useMemo(() => new MemoryEphemeralKeyStorage(), [])
|
|
193
|
+
|
|
194
|
+
// Current active storage based on persist setting
|
|
195
|
+
const ephemeralKeyStorage = state.persistEphemeralKeys && indexedDBStorage
|
|
196
|
+
? indexedDBStorage
|
|
197
|
+
: memoryStorage
|
|
198
|
+
|
|
199
|
+
// Single ephemeral key manager instance that we update storage on
|
|
200
|
+
const ephemeralKeyManagerRef = useRef<EphemeralKeyManager | null>(null)
|
|
201
|
+
if (!ephemeralKeyManagerRef.current) {
|
|
202
|
+
ephemeralKeyManagerRef.current = new EphemeralKeyManager(ephemeralKeyStorage)
|
|
203
|
+
}
|
|
204
|
+
const ephemeralKeyManager = ephemeralKeyManagerRef.current
|
|
205
|
+
|
|
103
206
|
const client = useMemo(
|
|
104
207
|
() => new KentuckySignerClient({ baseUrl }),
|
|
105
208
|
[baseUrl]
|
|
106
209
|
)
|
|
107
210
|
|
|
211
|
+
// Secure client for ephemeral key signing - uses shared key manager
|
|
212
|
+
const secureClient = useMemo(
|
|
213
|
+
() => new SecureKentuckySignerClient({
|
|
214
|
+
baseUrl,
|
|
215
|
+
ephemeralKeyManager,
|
|
216
|
+
}),
|
|
217
|
+
[baseUrl, ephemeralKeyManager]
|
|
218
|
+
)
|
|
219
|
+
|
|
108
220
|
const storage = useMemo(
|
|
109
221
|
() => (persistSession ? new LocalStorageTokenStorage(storageKeyPrefix) : null),
|
|
110
222
|
[persistSession, storageKeyPrefix]
|
|
111
223
|
)
|
|
112
224
|
|
|
225
|
+
// 2FA callback handler - shows prompt and waits for user input
|
|
226
|
+
const handle2FARequired = useCallback(async (requirements: {
|
|
227
|
+
totpRequired: boolean
|
|
228
|
+
pinRequired: boolean
|
|
229
|
+
pinLength: number
|
|
230
|
+
}): Promise<TwoFactorCodes | null> => {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
setState((s) => ({
|
|
233
|
+
...s,
|
|
234
|
+
twoFactorPrompt: {
|
|
235
|
+
isVisible: true,
|
|
236
|
+
totpRequired: requirements.totpRequired,
|
|
237
|
+
pinRequired: requirements.pinRequired,
|
|
238
|
+
pinLength: requirements.pinLength,
|
|
239
|
+
resolve,
|
|
240
|
+
},
|
|
241
|
+
}))
|
|
242
|
+
})
|
|
243
|
+
}, [])
|
|
244
|
+
|
|
113
245
|
// Create account from session
|
|
114
246
|
const createAccount = useCallback(
|
|
115
|
-
(session: AuthSession): KentuckySignerAccount => {
|
|
247
|
+
(session: AuthSession, useSecureClient: boolean = false): KentuckySignerAccount => {
|
|
116
248
|
return createKentuckySignerAccount({
|
|
117
249
|
config: { baseUrl, accountId: session.accountId },
|
|
118
250
|
session,
|
|
119
251
|
defaultChainId,
|
|
252
|
+
secureClient: useSecureClient ? secureClient : undefined,
|
|
120
253
|
onSessionExpired: async () => {
|
|
121
254
|
// Try to refresh the session
|
|
122
255
|
const newSession = await refreshSessionIfNeeded(session, baseUrl, 0)
|
|
@@ -129,9 +262,10 @@ export function KentuckySignerProvider({
|
|
|
129
262
|
}))
|
|
130
263
|
return newSession
|
|
131
264
|
},
|
|
265
|
+
on2FARequired: handle2FARequired,
|
|
132
266
|
})
|
|
133
267
|
},
|
|
134
|
-
[baseUrl, defaultChainId]
|
|
268
|
+
[baseUrl, defaultChainId, secureClient, handle2FARequired]
|
|
135
269
|
)
|
|
136
270
|
|
|
137
271
|
// Restore session from storage on mount
|
|
@@ -148,17 +282,23 @@ export function KentuckySignerProvider({
|
|
|
148
282
|
// Try to refresh
|
|
149
283
|
try {
|
|
150
284
|
const refreshed = await refreshSessionIfNeeded(session, baseUrl, 0)
|
|
151
|
-
const account = createAccount(refreshed)
|
|
152
285
|
localStorage.setItem(
|
|
153
286
|
`${storageKeyPrefix}_session`,
|
|
154
287
|
JSON.stringify(refreshed)
|
|
155
288
|
)
|
|
156
|
-
setState({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
289
|
+
setState((s) => {
|
|
290
|
+
const account = createAccount(refreshed, s.secureMode)
|
|
291
|
+
return {
|
|
292
|
+
isAuthenticating: false,
|
|
293
|
+
isAuthenticated: true,
|
|
294
|
+
session: refreshed,
|
|
295
|
+
account,
|
|
296
|
+
error: null,
|
|
297
|
+
ephemeralKeyBound: false,
|
|
298
|
+
secureMode: s.secureMode,
|
|
299
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
300
|
+
twoFactorPrompt: s.twoFactorPrompt,
|
|
301
|
+
}
|
|
162
302
|
})
|
|
163
303
|
} catch {
|
|
164
304
|
// Clear invalid session
|
|
@@ -167,13 +307,19 @@ export function KentuckySignerProvider({
|
|
|
167
307
|
return
|
|
168
308
|
}
|
|
169
309
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
310
|
+
setState((s) => {
|
|
311
|
+
const account = createAccount(session, s.secureMode)
|
|
312
|
+
return {
|
|
313
|
+
isAuthenticating: false,
|
|
314
|
+
isAuthenticated: true,
|
|
315
|
+
session,
|
|
316
|
+
account,
|
|
317
|
+
error: null,
|
|
318
|
+
ephemeralKeyBound: false,
|
|
319
|
+
secureMode: s.secureMode,
|
|
320
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
321
|
+
twoFactorPrompt: s.twoFactorPrompt,
|
|
322
|
+
}
|
|
177
323
|
})
|
|
178
324
|
} catch {
|
|
179
325
|
localStorage.removeItem(`${storageKeyPrefix}_session`)
|
|
@@ -183,19 +329,35 @@ export function KentuckySignerProvider({
|
|
|
183
329
|
restoreSession()
|
|
184
330
|
}, [storage, storageKeyPrefix, baseUrl, createAccount])
|
|
185
331
|
|
|
186
|
-
// Authenticate with passkey
|
|
332
|
+
// Authenticate with passkey or existing session
|
|
187
333
|
const authenticate = useCallback(
|
|
188
|
-
async (accountId: string, options?: { rpId?: string }) => {
|
|
334
|
+
async (accountId: string, options?: { rpId?: string; session?: AuthSession }) => {
|
|
189
335
|
setState((s) => ({ ...s, isAuthenticating: true, error: null }))
|
|
190
336
|
|
|
191
337
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
338
|
+
let session: AuthSession
|
|
339
|
+
let ephemeralKeyBound = false
|
|
340
|
+
|
|
341
|
+
if (options?.session) {
|
|
342
|
+
// Use provided session (no ephemeral binding possible)
|
|
343
|
+
session = options.session
|
|
344
|
+
} else {
|
|
345
|
+
// Get ephemeral public key if secure mode is enabled
|
|
346
|
+
let ephemeralPublicKey: string | undefined
|
|
347
|
+
if (state.secureMode) {
|
|
348
|
+
ephemeralPublicKey = await ephemeralKeyManager.getPublicKey()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Authenticate with passkey
|
|
352
|
+
session = await authenticateWithPasskey({
|
|
353
|
+
baseUrl,
|
|
354
|
+
accountId,
|
|
355
|
+
rpId: options?.rpId,
|
|
356
|
+
ephemeralPublicKey,
|
|
357
|
+
})
|
|
197
358
|
|
|
198
|
-
|
|
359
|
+
ephemeralKeyBound = !!ephemeralPublicKey
|
|
360
|
+
}
|
|
199
361
|
|
|
200
362
|
// Persist session
|
|
201
363
|
if (storage) {
|
|
@@ -205,12 +367,19 @@ export function KentuckySignerProvider({
|
|
|
205
367
|
)
|
|
206
368
|
}
|
|
207
369
|
|
|
208
|
-
setState({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
370
|
+
setState((s) => {
|
|
371
|
+
const account = createAccount(session, s.secureMode)
|
|
372
|
+
return {
|
|
373
|
+
isAuthenticating: false,
|
|
374
|
+
isAuthenticated: true,
|
|
375
|
+
session,
|
|
376
|
+
account,
|
|
377
|
+
error: null,
|
|
378
|
+
ephemeralKeyBound,
|
|
379
|
+
secureMode: s.secureMode,
|
|
380
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
381
|
+
twoFactorPrompt: s.twoFactorPrompt,
|
|
382
|
+
}
|
|
214
383
|
})
|
|
215
384
|
} catch (error) {
|
|
216
385
|
setState((s) => ({
|
|
@@ -221,7 +390,7 @@ export function KentuckySignerProvider({
|
|
|
221
390
|
throw error
|
|
222
391
|
}
|
|
223
392
|
},
|
|
224
|
-
[baseUrl, createAccount, storage, storageKeyPrefix]
|
|
393
|
+
[baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
|
|
225
394
|
)
|
|
226
395
|
|
|
227
396
|
// Logout
|
|
@@ -239,14 +408,23 @@ export function KentuckySignerProvider({
|
|
|
239
408
|
localStorage.removeItem(`${storageKeyPrefix}_session`)
|
|
240
409
|
}
|
|
241
410
|
|
|
242
|
-
|
|
411
|
+
// Clear ephemeral keys if using secure mode
|
|
412
|
+
if (ephemeralKeyManager) {
|
|
413
|
+
await ephemeralKeyManager.clear()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
setState((s) => ({
|
|
243
417
|
isAuthenticating: false,
|
|
244
418
|
isAuthenticated: false,
|
|
245
419
|
session: null,
|
|
246
420
|
account: null,
|
|
247
421
|
error: null,
|
|
248
|
-
|
|
249
|
-
|
|
422
|
+
ephemeralKeyBound: false,
|
|
423
|
+
secureMode: s.secureMode,
|
|
424
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
425
|
+
twoFactorPrompt: defaultTwoFactorPrompt,
|
|
426
|
+
}))
|
|
427
|
+
}, [client, state.session, storage, storageKeyPrefix, ephemeralKeyManager])
|
|
250
428
|
|
|
251
429
|
// Refresh session
|
|
252
430
|
const refreshSession = useCallback(async () => {
|
|
@@ -256,8 +434,6 @@ export function KentuckySignerProvider({
|
|
|
256
434
|
const refreshed = await refreshSessionIfNeeded(state.session, baseUrl)
|
|
257
435
|
|
|
258
436
|
if (refreshed !== state.session) {
|
|
259
|
-
const account = createAccount(refreshed)
|
|
260
|
-
|
|
261
437
|
if (storage) {
|
|
262
438
|
localStorage.setItem(
|
|
263
439
|
`${storageKeyPrefix}_session`,
|
|
@@ -265,11 +441,14 @@ export function KentuckySignerProvider({
|
|
|
265
441
|
)
|
|
266
442
|
}
|
|
267
443
|
|
|
268
|
-
setState((s) =>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
444
|
+
setState((s) => {
|
|
445
|
+
const account = createAccount(refreshed, s.secureMode)
|
|
446
|
+
return {
|
|
447
|
+
...s,
|
|
448
|
+
session: refreshed,
|
|
449
|
+
account,
|
|
450
|
+
}
|
|
451
|
+
})
|
|
273
452
|
}
|
|
274
453
|
} catch (error) {
|
|
275
454
|
setState((s) => ({ ...s, error: error as Error }))
|
|
@@ -281,15 +460,151 @@ export function KentuckySignerProvider({
|
|
|
281
460
|
setState((s) => ({ ...s, error: null }))
|
|
282
461
|
}, [])
|
|
283
462
|
|
|
463
|
+
// Toggle secure mode - recreates account with new client
|
|
464
|
+
const setSecureMode = useCallback((enabled: boolean) => {
|
|
465
|
+
// Persist the setting to localStorage
|
|
466
|
+
if (typeof localStorage !== 'undefined') {
|
|
467
|
+
localStorage.setItem(`${storageKeyPrefix}_secure_mode`, String(enabled))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
setState((s) => {
|
|
471
|
+
// Recreate account with new secure mode setting if authenticated
|
|
472
|
+
const newAccount = s.session ? createAccount(s.session, enabled) : null
|
|
473
|
+
return {
|
|
474
|
+
...s,
|
|
475
|
+
secureMode: enabled,
|
|
476
|
+
account: newAccount,
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
}, [createAccount, storageKeyPrefix])
|
|
480
|
+
|
|
481
|
+
// Toggle ephemeral key persistence - migrates key between IndexedDB and memory storage
|
|
482
|
+
const setPersistEphemeralKeys = useCallback(async (enabled: boolean) => {
|
|
483
|
+
// Determine the new storage backend
|
|
484
|
+
const newStorage = enabled && indexedDBStorage
|
|
485
|
+
? indexedDBStorage
|
|
486
|
+
: memoryStorage
|
|
487
|
+
|
|
488
|
+
// Migrate the key to the new storage (preserves existing key)
|
|
489
|
+
await ephemeralKeyManager.migrateStorage(newStorage)
|
|
490
|
+
|
|
491
|
+
// Persist the setting to localStorage
|
|
492
|
+
if (typeof localStorage !== 'undefined') {
|
|
493
|
+
localStorage.setItem(`${storageKeyPrefix}_persist_ephemeral_keys`, String(enabled))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Update state
|
|
497
|
+
setState((s) => ({
|
|
498
|
+
...s,
|
|
499
|
+
persistEphemeralKeys: enabled,
|
|
500
|
+
}))
|
|
501
|
+
}, [ephemeralKeyManager, storageKeyPrefix, indexedDBStorage, memoryStorage])
|
|
502
|
+
|
|
503
|
+
// Get ephemeral public key for external auth flows
|
|
504
|
+
const getEphemeralPublicKey = useCallback(async (): Promise<string | undefined> => {
|
|
505
|
+
if (!state.secureMode) {
|
|
506
|
+
return undefined
|
|
507
|
+
}
|
|
508
|
+
return ephemeralKeyManager.getPublicKey()
|
|
509
|
+
}, [state.secureMode, ephemeralKeyManager])
|
|
510
|
+
|
|
511
|
+
// Authenticate with password
|
|
512
|
+
const authenticatePassword = useCallback(
|
|
513
|
+
async (accountId: string, password: string) => {
|
|
514
|
+
setState((s) => ({ ...s, isAuthenticating: true, error: null }))
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
// Get ephemeral public key if secure mode is enabled
|
|
518
|
+
let ephemeralPublicKey: string | undefined
|
|
519
|
+
console.log('[KentuckySigner] authenticatePassword - secureMode:', state.secureMode)
|
|
520
|
+
if (state.secureMode) {
|
|
521
|
+
ephemeralPublicKey = await ephemeralKeyManager.getPublicKey()
|
|
522
|
+
console.log('[KentuckySigner] Got ephemeral public key for binding:', ephemeralPublicKey?.substring(0, 50) + '...')
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Authenticate with password
|
|
526
|
+
const session = await authWithPassword({
|
|
527
|
+
baseUrl,
|
|
528
|
+
accountId,
|
|
529
|
+
password,
|
|
530
|
+
ephemeralPublicKey,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const ephemeralKeyBound = !!ephemeralPublicKey
|
|
534
|
+
|
|
535
|
+
// Persist session
|
|
536
|
+
if (storage) {
|
|
537
|
+
localStorage.setItem(
|
|
538
|
+
`${storageKeyPrefix}_session`,
|
|
539
|
+
JSON.stringify(session)
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
setState((s) => {
|
|
544
|
+
const account = createAccount(session, s.secureMode)
|
|
545
|
+
return {
|
|
546
|
+
isAuthenticating: false,
|
|
547
|
+
isAuthenticated: true,
|
|
548
|
+
session,
|
|
549
|
+
account,
|
|
550
|
+
error: null,
|
|
551
|
+
ephemeralKeyBound,
|
|
552
|
+
secureMode: s.secureMode,
|
|
553
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
554
|
+
twoFactorPrompt: s.twoFactorPrompt,
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
} catch (error) {
|
|
558
|
+
setState((s) => ({
|
|
559
|
+
...s,
|
|
560
|
+
isAuthenticating: false,
|
|
561
|
+
error: error as Error,
|
|
562
|
+
}))
|
|
563
|
+
throw error
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
[baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
// Submit 2FA codes
|
|
570
|
+
const submit2FA = useCallback((codes: TwoFactorCodes) => {
|
|
571
|
+
const { resolve } = state.twoFactorPrompt
|
|
572
|
+
if (resolve) {
|
|
573
|
+
resolve(codes)
|
|
574
|
+
}
|
|
575
|
+
setState((s) => ({
|
|
576
|
+
...s,
|
|
577
|
+
twoFactorPrompt: defaultTwoFactorPrompt,
|
|
578
|
+
}))
|
|
579
|
+
}, [state.twoFactorPrompt])
|
|
580
|
+
|
|
581
|
+
// Cancel 2FA prompt
|
|
582
|
+
const cancel2FA = useCallback(() => {
|
|
583
|
+
const { resolve } = state.twoFactorPrompt
|
|
584
|
+
if (resolve) {
|
|
585
|
+
resolve(null)
|
|
586
|
+
}
|
|
587
|
+
setState((s) => ({
|
|
588
|
+
...s,
|
|
589
|
+
twoFactorPrompt: defaultTwoFactorPrompt,
|
|
590
|
+
}))
|
|
591
|
+
}, [state.twoFactorPrompt])
|
|
592
|
+
|
|
284
593
|
const value: KentuckySignerContextValue = useMemo(
|
|
285
594
|
() => ({
|
|
286
595
|
...state,
|
|
287
596
|
authenticate,
|
|
597
|
+
authenticatePassword,
|
|
288
598
|
logout,
|
|
289
599
|
refreshSession,
|
|
290
600
|
clearError,
|
|
601
|
+
setSecureMode,
|
|
602
|
+
setPersistEphemeralKeys,
|
|
603
|
+
getEphemeralPublicKey,
|
|
604
|
+
submit2FA,
|
|
605
|
+
cancel2FA,
|
|
291
606
|
}),
|
|
292
|
-
[state, authenticate, logout, refreshSession, clearError]
|
|
607
|
+
[state, authenticate, authenticatePassword, logout, refreshSession, clearError, setSecureMode, setPersistEphemeralKeys, getEphemeralPublicKey, submit2FA, cancel2FA]
|
|
293
608
|
)
|
|
294
609
|
|
|
295
610
|
return (
|
package/src/react/hooks.ts
CHANGED
|
@@ -34,10 +34,21 @@ export function useKentuckySigner() {
|
|
|
34
34
|
session: context.session,
|
|
35
35
|
account: context.account,
|
|
36
36
|
error: context.error,
|
|
37
|
+
ephemeralKeyBound: context.ephemeralKeyBound,
|
|
37
38
|
authenticate: context.authenticate,
|
|
39
|
+
authenticatePassword: context.authenticatePassword,
|
|
38
40
|
logout: context.logout,
|
|
39
41
|
refreshSession: context.refreshSession,
|
|
40
42
|
clearError: context.clearError,
|
|
43
|
+
secureMode: context.secureMode,
|
|
44
|
+
setSecureMode: context.setSecureMode,
|
|
45
|
+
persistEphemeralKeys: context.persistEphemeralKeys,
|
|
46
|
+
setPersistEphemeralKeys: context.setPersistEphemeralKeys,
|
|
47
|
+
getEphemeralPublicKey: context.getEphemeralPublicKey,
|
|
48
|
+
// 2FA support
|
|
49
|
+
twoFactorPrompt: context.twoFactorPrompt,
|
|
50
|
+
submit2FA: context.submit2FA,
|
|
51
|
+
cancel2FA: context.cancel2FA,
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|