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.
@@ -5,7 +5,8 @@ import {
5
5
  useState,
6
6
  useCallback,
7
7
  useEffect,
8
- useMemo
8
+ useMemo,
9
+ useRef
9
10
  } from "react";
10
11
 
11
12
  // src/client.ts
@@ -20,7 +21,7 @@ var KentuckySignerError = class extends Error {
20
21
  var KentuckySignerClient = class {
21
22
  constructor(options) {
22
23
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
23
- this.fetchImpl = options.fetch ?? globalThis.fetch;
24
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
24
25
  this.timeout = options.timeout ?? 3e4;
25
26
  }
26
27
  /**
@@ -75,9 +76,831 @@ var KentuckySignerClient = class {
75
76
  *
76
77
  * @param accountId - Account ID to authenticate
77
78
  * @param credential - WebAuthn credential from navigator.credentials.get()
79
+ * @param ephemeralPublicKey - Optional ephemeral public key for secure mode binding
78
80
  * @returns Authentication response with JWT token
79
81
  */
82
+ async authenticatePasskey(accountId, credential, ephemeralPublicKey) {
83
+ const body = {
84
+ account_id: accountId,
85
+ credential_id: credential.credentialId,
86
+ client_data_json: credential.clientDataJSON,
87
+ authenticator_data: credential.authenticatorData,
88
+ signature: credential.signature,
89
+ user_handle: credential.userHandle
90
+ };
91
+ if (ephemeralPublicKey) {
92
+ body.ephemeral_public_key = ephemeralPublicKey;
93
+ }
94
+ return this.request("/api/auth/passkey", {
95
+ method: "POST",
96
+ body: JSON.stringify(body)
97
+ });
98
+ }
99
+ /**
100
+ * Refresh an authentication token
101
+ *
102
+ * @param token - Current JWT token
103
+ * @returns New authentication response with fresh token
104
+ */
105
+ async refreshToken(token) {
106
+ return this.request("/api/auth/refresh", {
107
+ method: "POST",
108
+ token
109
+ });
110
+ }
111
+ /**
112
+ * Logout and invalidate token
113
+ *
114
+ * @param token - JWT token to invalidate
115
+ */
116
+ async logout(token) {
117
+ await this.request("/api/auth/logout", {
118
+ method: "POST",
119
+ token
120
+ });
121
+ }
122
+ /**
123
+ * Get account information
124
+ *
125
+ * @param accountId - Account ID
126
+ * @param token - JWT token
127
+ * @returns Account info with addresses and passkeys
128
+ */
129
+ async getAccountInfo(accountId, token) {
130
+ return this.request(`/api/accounts/${accountId}`, {
131
+ method: "GET",
132
+ token
133
+ });
134
+ }
135
+ /**
136
+ * Check if an account exists
137
+ *
138
+ * @param accountId - Account ID
139
+ * @param token - JWT token
140
+ * @returns True if account exists
141
+ */
142
+ async accountExists(accountId, token) {
143
+ try {
144
+ await this.request(`/api/accounts/${accountId}`, {
145
+ method: "HEAD",
146
+ token
147
+ });
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+ /**
154
+ * Sign an EVM transaction hash
155
+ *
156
+ * @param request - Sign request with tx_hash and chain_id
157
+ * @param token - JWT token
158
+ * @returns Signature response with r, s, v components
159
+ */
160
+ async signEvmTransaction(request, token) {
161
+ return this.request("/api/sign/evm", {
162
+ method: "POST",
163
+ token,
164
+ body: JSON.stringify(request)
165
+ });
166
+ }
167
+ /**
168
+ * Sign a raw hash for EVM
169
+ *
170
+ * Convenience method that wraps signEvmTransaction.
171
+ *
172
+ * @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
173
+ * @param chainId - Chain ID
174
+ * @param token - JWT token
175
+ * @returns Full signature (hex encoded with 0x prefix)
176
+ */
177
+ async signHash(hash, chainId, token) {
178
+ const response = await this.signEvmTransaction(
179
+ { tx_hash: hash, chain_id: chainId },
180
+ token
181
+ );
182
+ return response.signature.full;
183
+ }
184
+ /**
185
+ * Create a new account with passkey authentication
186
+ *
187
+ * @param attestationObject - Base64url encoded attestation object from WebAuthn
188
+ * @param label - Optional label for the passkey (defaults to "Owner Passkey")
189
+ * @returns Account creation response with account ID and addresses
190
+ */
191
+ async createAccountWithPasskey(attestationObject, label) {
192
+ const body = {
193
+ attestation_object: attestationObject
194
+ };
195
+ if (label) {
196
+ body.label = label;
197
+ }
198
+ return this.request("/api/accounts/create/passkey", {
199
+ method: "POST",
200
+ body: JSON.stringify(body)
201
+ });
202
+ }
203
+ /**
204
+ * Create a new account with password authentication
205
+ *
206
+ * @param request - Password and confirmation
207
+ * @returns Account creation response with account ID and addresses
208
+ */
209
+ async createAccountWithPassword(request) {
210
+ return this.request("/api/accounts/create/password", {
211
+ method: "POST",
212
+ body: JSON.stringify(request)
213
+ });
214
+ }
215
+ /**
216
+ * Authenticate with password
217
+ *
218
+ * @param request - Account ID and password
219
+ * @returns Authentication response with JWT token
220
+ */
221
+ async authenticatePassword(request) {
222
+ return this.request("/api/auth/password", {
223
+ method: "POST",
224
+ body: JSON.stringify(request)
225
+ });
226
+ }
227
+ /**
228
+ * Add password authentication to an existing account
229
+ *
230
+ * Enables password-based authentication for an account that was created
231
+ * with passkey-only authentication.
232
+ *
233
+ * @param accountId - Account ID
234
+ * @param request - Password and confirmation
235
+ * @param token - JWT token
236
+ * @returns Success response
237
+ */
238
+ async addPassword(accountId, request, token) {
239
+ return this.request(`/api/accounts/${accountId}/password`, {
240
+ method: "POST",
241
+ token,
242
+ body: JSON.stringify(request)
243
+ });
244
+ }
245
+ /**
246
+ * Get extended account information including auth config
247
+ *
248
+ * @param accountId - Account ID
249
+ * @param token - JWT token
250
+ * @returns Extended account info with auth config and passkey count
251
+ */
252
+ async getAccountInfoExtended(accountId, token) {
253
+ return this.request(`/api/accounts/${accountId}`, {
254
+ method: "GET",
255
+ token
256
+ });
257
+ }
258
+ /**
259
+ * Add a recovery passkey to an existing account
260
+ *
261
+ * @param accountId - Account ID
262
+ * @param request - Passkey data (COSE public key, credential ID, algorithm, label)
263
+ * @param token - JWT token
264
+ * @returns Success response with label
265
+ */
266
+ async addPasskey(accountId, request, token) {
267
+ return this.request(`/api/accounts/${accountId}/passkeys`, {
268
+ method: "POST",
269
+ token,
270
+ body: JSON.stringify(request)
271
+ });
272
+ }
273
+ /**
274
+ * Remove a passkey from an account
275
+ *
276
+ * Note: Cannot remove the owner passkey (index 0)
277
+ *
278
+ * @param accountId - Account ID
279
+ * @param passkeyIndex - Index of passkey to remove (1-3 for recovery passkeys)
280
+ * @param token - JWT token
281
+ * @returns Success response
282
+ */
283
+ async removePasskey(accountId, passkeyIndex, token) {
284
+ return this.request(`/api/accounts/${accountId}/passkeys/${passkeyIndex}`, {
285
+ method: "DELETE",
286
+ token
287
+ });
288
+ }
289
+ /**
290
+ * Health check
291
+ *
292
+ * @returns True if the API is healthy
293
+ */
294
+ async healthCheck() {
295
+ try {
296
+ const response = await this.request("/api/health", {
297
+ method: "GET"
298
+ });
299
+ return response.status === "ok";
300
+ } catch {
301
+ return false;
302
+ }
303
+ }
304
+ /**
305
+ * Get API version
306
+ *
307
+ * @returns Version string
308
+ */
309
+ async getVersion() {
310
+ const response = await this.request("/api/version", {
311
+ method: "GET"
312
+ });
313
+ return response.version;
314
+ }
315
+ // ============================================================================
316
+ // Guardian Management
317
+ // ============================================================================
318
+ /**
319
+ * Add a guardian passkey to an account
320
+ *
321
+ * Guardians can participate in account recovery but cannot access the wallet.
322
+ * An account can have up to 3 guardians (indices 1-3).
323
+ *
324
+ * @param request - Guardian data (attestation object and optional label)
325
+ * @param token - JWT token
326
+ * @returns Success response with guardian index and count
327
+ */
328
+ async addGuardian(request, token) {
329
+ return this.request("/api/guardians/add", {
330
+ method: "POST",
331
+ token,
332
+ body: JSON.stringify(request)
333
+ });
334
+ }
335
+ /**
336
+ * Remove a guardian passkey from an account
337
+ *
338
+ * Cannot remove guardians during an active recovery.
339
+ *
340
+ * @param guardianIndex - Index of guardian to remove (1-3)
341
+ * @param token - JWT token
342
+ * @returns Success response with remaining guardian count
343
+ */
344
+ async removeGuardian(guardianIndex, token) {
345
+ return this.request("/api/guardians/remove", {
346
+ method: "POST",
347
+ token,
348
+ body: JSON.stringify({ guardian_index: guardianIndex })
349
+ });
350
+ }
351
+ /**
352
+ * Get guardians for an account
353
+ *
354
+ * @param token - JWT token
355
+ * @returns Guardian list with indices and labels
356
+ */
357
+ async getGuardians(token) {
358
+ return this.request("/api/guardians", {
359
+ method: "GET",
360
+ token
361
+ });
362
+ }
363
+ // ============================================================================
364
+ // Account Recovery
365
+ // ============================================================================
366
+ /**
367
+ * Initiate account recovery
368
+ *
369
+ * Call this when you've lost access to your account. You'll need to register
370
+ * a new passkey which will become the new owner passkey after recovery completes.
371
+ *
372
+ * @param accountId - Account ID to recover
373
+ * @param attestationObject - WebAuthn attestation object for new owner passkey
374
+ * @param label - Optional label for new owner passkey
375
+ * @returns Challenges for guardians to sign, threshold, and timelock info
376
+ */
377
+ async initiateRecovery(accountId, attestationObject, label) {
378
+ const body = {
379
+ account_id: accountId,
380
+ attestation_object: attestationObject
381
+ };
382
+ if (label) {
383
+ body.label = label;
384
+ }
385
+ return this.request("/api/recovery/initiate", {
386
+ method: "POST",
387
+ body: JSON.stringify(body)
388
+ });
389
+ }
390
+ /**
391
+ * Submit a guardian signature for recovery
392
+ *
393
+ * Each guardian must sign their challenge using their passkey.
394
+ *
395
+ * @param request - Guardian signature data
396
+ * @returns Current verification status
397
+ */
398
+ async verifyGuardian(request) {
399
+ return this.request("/api/recovery/verify", {
400
+ method: "POST",
401
+ body: JSON.stringify(request)
402
+ });
403
+ }
404
+ /**
405
+ * Get recovery status for an account
406
+ *
407
+ * @param accountId - Account ID to check
408
+ * @returns Recovery status including verification count and timelock
409
+ */
410
+ async getRecoveryStatus(accountId) {
411
+ return this.request("/api/recovery/status", {
412
+ method: "POST",
413
+ body: JSON.stringify({ account_id: accountId })
414
+ });
415
+ }
416
+ /**
417
+ * Complete account recovery
418
+ *
419
+ * Call this after enough guardians have verified and the timelock has expired.
420
+ * The new owner passkey will replace the old one.
421
+ *
422
+ * @param accountId - Account ID to complete recovery for
423
+ * @returns Success message
424
+ */
425
+ async completeRecovery(accountId) {
426
+ return this.request("/api/recovery/complete", {
427
+ method: "POST",
428
+ body: JSON.stringify({ account_id: accountId })
429
+ });
430
+ }
431
+ /**
432
+ * Cancel a pending recovery
433
+ *
434
+ * Only the current owner (with valid auth) can cancel a recovery.
435
+ * Use this if you regain access and want to stop an unauthorized recovery.
436
+ *
437
+ * @param token - JWT token (must be authenticated as owner)
438
+ * @returns Success message
439
+ */
440
+ async cancelRecovery(token) {
441
+ return this.request("/api/recovery/cancel", {
442
+ method: "POST",
443
+ token
444
+ });
445
+ }
446
+ // ============================================================================
447
+ // Two-Factor Authentication (2FA)
448
+ // ============================================================================
449
+ /**
450
+ * Get 2FA status for the authenticated account
451
+ *
452
+ * @param token - JWT token
453
+ * @returns 2FA status including TOTP and PIN enablement
454
+ */
455
+ async get2FAStatus(token) {
456
+ return this.request("/api/2fa/status", {
457
+ method: "GET",
458
+ token
459
+ });
460
+ }
461
+ /**
462
+ * Setup TOTP for the account
463
+ *
464
+ * Returns an otpauth:// URI for QR code generation and a base32 secret
465
+ * for manual entry. After setup, call enableTOTP with a valid code to
466
+ * complete the setup.
467
+ *
468
+ * @param token - JWT token
469
+ * @returns TOTP setup response with URI and secret
470
+ */
471
+ async setupTOTP(token) {
472
+ return this.request("/api/2fa/totp/setup", {
473
+ method: "POST",
474
+ token
475
+ });
476
+ }
477
+ /**
478
+ * Verify and enable TOTP for the account
479
+ *
480
+ * Call this after setupTOTP with a valid code from the authenticator app.
481
+ *
482
+ * @param code - 6-digit TOTP code from authenticator app
483
+ * @param token - JWT token
484
+ * @returns Success response
485
+ */
486
+ async enableTOTP(code, token) {
487
+ return this.request("/api/2fa/totp/enable", {
488
+ method: "POST",
489
+ token,
490
+ body: JSON.stringify({ code })
491
+ });
492
+ }
493
+ /**
494
+ * Disable TOTP for the account
495
+ *
496
+ * Requires a valid TOTP code for confirmation.
497
+ *
498
+ * @param code - Current 6-digit TOTP code
499
+ * @param token - JWT token
500
+ * @returns Success response
501
+ */
502
+ async disableTOTP(code, token) {
503
+ return this.request("/api/2fa/totp/disable", {
504
+ method: "POST",
505
+ token,
506
+ body: JSON.stringify({ code })
507
+ });
508
+ }
509
+ /**
510
+ * Verify a TOTP code
511
+ *
512
+ * Use this to verify a TOTP code without performing any other operation.
513
+ *
514
+ * @param code - 6-digit TOTP code
515
+ * @param token - JWT token
516
+ * @returns Verification result
517
+ */
518
+ async verifyTOTP(code, token) {
519
+ return this.request("/api/2fa/totp/verify", {
520
+ method: "POST",
521
+ token,
522
+ body: JSON.stringify({ code })
523
+ });
524
+ }
525
+ /**
526
+ * Setup PIN for the account
527
+ *
528
+ * Sets a 4-digit or 6-digit PIN. If a PIN is already set, this replaces it.
529
+ *
530
+ * @param pin - 4 or 6 digit PIN
531
+ * @param token - JWT token
532
+ * @returns Success response with PIN length
533
+ */
534
+ async setupPIN(pin, token) {
535
+ return this.request("/api/2fa/pin/setup", {
536
+ method: "POST",
537
+ token,
538
+ body: JSON.stringify({ pin })
539
+ });
540
+ }
541
+ /**
542
+ * Disable PIN for the account
543
+ *
544
+ * Requires the current PIN for confirmation.
545
+ *
546
+ * @param pin - Current PIN
547
+ * @param token - JWT token
548
+ * @returns Success response
549
+ */
550
+ async disablePIN(pin, token) {
551
+ return this.request("/api/2fa/pin/disable", {
552
+ method: "POST",
553
+ token,
554
+ body: JSON.stringify({ pin })
555
+ });
556
+ }
557
+ /**
558
+ * Verify a PIN
559
+ *
560
+ * Use this to verify a PIN without performing any other operation.
561
+ *
562
+ * @param pin - PIN to verify
563
+ * @param token - JWT token
564
+ * @returns Verification result
565
+ */
566
+ async verifyPIN(pin, token) {
567
+ return this.request("/api/2fa/pin/verify", {
568
+ method: "POST",
569
+ token,
570
+ body: JSON.stringify({ pin })
571
+ });
572
+ }
573
+ /**
574
+ * Sign an EVM transaction with 2FA
575
+ *
576
+ * Use this method when 2FA is enabled. If 2FA is not enabled,
577
+ * you can use the regular signEvmTransaction method instead.
578
+ *
579
+ * @param request - Sign request including tx_hash, chain_id, and optional 2FA codes
580
+ * @param token - JWT token
581
+ * @returns Signature response with r, s, v components
582
+ */
583
+ async signEvmTransactionWith2FA(request, token) {
584
+ return this.request("/api/sign/evm", {
585
+ method: "POST",
586
+ token,
587
+ body: JSON.stringify(request)
588
+ });
589
+ }
590
+ };
591
+
592
+ // src/utils.ts
593
+ function base64UrlEncode(data) {
594
+ let base64;
595
+ if (typeof Buffer !== "undefined") {
596
+ base64 = Buffer.from(data).toString("base64");
597
+ } else {
598
+ const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
599
+ base64 = btoa(binary);
600
+ }
601
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
602
+ }
603
+ function base64UrlDecode(str) {
604
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
605
+ const padding = (4 - base64.length % 4) % 4;
606
+ base64 += "=".repeat(padding);
607
+ if (typeof Buffer !== "undefined") {
608
+ return new Uint8Array(Buffer.from(base64, "base64"));
609
+ } else {
610
+ const binary = atob(base64);
611
+ const bytes = new Uint8Array(binary.length);
612
+ for (let i = 0; i < binary.length; i++) {
613
+ bytes[i] = binary.charCodeAt(i);
614
+ }
615
+ return bytes;
616
+ }
617
+ }
618
+
619
+ // src/ephemeral.ts
620
+ function isWebCryptoAvailable() {
621
+ return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && typeof crypto.getRandomValues !== "undefined";
622
+ }
623
+ async function generateEphemeralKeyPair() {
624
+ if (!isWebCryptoAvailable()) {
625
+ throw new Error("WebCrypto is not available in this environment");
626
+ }
627
+ const keyPair = await crypto.subtle.generateKey(
628
+ {
629
+ name: "ECDSA",
630
+ namedCurve: "P-256"
631
+ },
632
+ true,
633
+ // extractable (only for public key export)
634
+ ["sign", "verify"]
635
+ );
636
+ const publicKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
637
+ const publicKeyBase64 = base64UrlEncode(new Uint8Array(publicKeyBuffer));
638
+ return {
639
+ publicKey: publicKeyBase64,
640
+ privateKey: keyPair.privateKey,
641
+ algorithm: "ES256",
642
+ createdAt: Date.now()
643
+ };
644
+ }
645
+ async function signPayload(payload, keyPair) {
646
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
647
+ const timestamp = Date.now();
648
+ const message = `${timestamp}.${payloadString}`;
649
+ const messageBytes = new TextEncoder().encode(message);
650
+ const signatureBuffer = await crypto.subtle.sign(
651
+ {
652
+ name: "ECDSA",
653
+ hash: "SHA-256"
654
+ },
655
+ keyPair.privateKey,
656
+ messageBytes
657
+ );
658
+ const signatureBase64 = base64UrlEncode(new Uint8Array(signatureBuffer));
659
+ return {
660
+ payload: payloadString,
661
+ signature: signatureBase64,
662
+ timestamp
663
+ };
664
+ }
665
+ var EPHEMERAL_KEY_STORAGE_KEY = "kentucky_signer_ephemeral";
666
+ var MemoryEphemeralKeyStorage = class {
667
+ constructor() {
668
+ this.keyPair = null;
669
+ }
670
+ async save(keyPair) {
671
+ this.keyPair = keyPair;
672
+ }
673
+ async load() {
674
+ return this.keyPair;
675
+ }
676
+ async clear() {
677
+ this.keyPair = null;
678
+ }
679
+ };
680
+ var IndexedDBEphemeralKeyStorage = class {
681
+ constructor() {
682
+ this.dbName = "kentucky_signer_ephemeral_keys";
683
+ this.storeName = "keys";
684
+ }
685
+ async getDB() {
686
+ return new Promise((resolve, reject) => {
687
+ const request = indexedDB.open(this.dbName, 1);
688
+ request.onerror = () => reject(request.error);
689
+ request.onsuccess = () => resolve(request.result);
690
+ request.onupgradeneeded = () => {
691
+ const db = request.result;
692
+ if (!db.objectStoreNames.contains(this.storeName)) {
693
+ db.createObjectStore(this.storeName);
694
+ }
695
+ };
696
+ });
697
+ }
698
+ async save(keyPair) {
699
+ const db = await this.getDB();
700
+ return new Promise((resolve, reject) => {
701
+ const tx = db.transaction(this.storeName, "readwrite");
702
+ const store = tx.objectStore(this.storeName);
703
+ const data = {
704
+ publicKey: keyPair.publicKey,
705
+ privateKey: keyPair.privateKey,
706
+ algorithm: keyPair.algorithm,
707
+ createdAt: keyPair.createdAt
708
+ };
709
+ const request = store.put(data, EPHEMERAL_KEY_STORAGE_KEY);
710
+ request.onerror = () => reject(request.error);
711
+ request.onsuccess = () => resolve();
712
+ });
713
+ }
714
+ async load() {
715
+ const db = await this.getDB();
716
+ return new Promise((resolve, reject) => {
717
+ const tx = db.transaction(this.storeName, "readonly");
718
+ const store = tx.objectStore(this.storeName);
719
+ const request = store.get(EPHEMERAL_KEY_STORAGE_KEY);
720
+ request.onerror = () => reject(request.error);
721
+ request.onsuccess = () => {
722
+ if (request.result) {
723
+ resolve({
724
+ publicKey: request.result.publicKey,
725
+ privateKey: request.result.privateKey,
726
+ algorithm: request.result.algorithm,
727
+ createdAt: request.result.createdAt
728
+ });
729
+ } else {
730
+ resolve(null);
731
+ }
732
+ };
733
+ });
734
+ }
735
+ async clear() {
736
+ const db = await this.getDB();
737
+ return new Promise((resolve, reject) => {
738
+ const tx = db.transaction(this.storeName, "readwrite");
739
+ const store = tx.objectStore(this.storeName);
740
+ const request = store.delete(EPHEMERAL_KEY_STORAGE_KEY);
741
+ request.onerror = () => reject(request.error);
742
+ request.onsuccess = () => resolve();
743
+ });
744
+ }
745
+ };
746
+ var EphemeralKeyManager = class {
747
+ constructor(storage) {
748
+ this.keyPair = null;
749
+ this.storage = storage ?? new MemoryEphemeralKeyStorage();
750
+ }
751
+ /**
752
+ * Get or generate ephemeral key pair
753
+ */
754
+ async getKeyPair() {
755
+ if (!this.keyPair) {
756
+ this.keyPair = await this.storage.load();
757
+ }
758
+ if (!this.keyPair) {
759
+ this.keyPair = await generateEphemeralKeyPair();
760
+ await this.storage.save(this.keyPair);
761
+ }
762
+ return this.keyPair;
763
+ }
764
+ /**
765
+ * Get the public key for authentication
766
+ */
767
+ async getPublicKey() {
768
+ const keyPair = await this.getKeyPair();
769
+ return keyPair.publicKey;
770
+ }
771
+ /**
772
+ * Sign a request payload
773
+ */
774
+ async signPayload(payload) {
775
+ const keyPair = await this.getKeyPair();
776
+ return signPayload(payload, keyPair);
777
+ }
778
+ /**
779
+ * Rotate the key pair (generate new keys)
780
+ */
781
+ async rotate() {
782
+ await this.storage.clear();
783
+ this.keyPair = await generateEphemeralKeyPair();
784
+ await this.storage.save(this.keyPair);
785
+ return this.keyPair;
786
+ }
787
+ /**
788
+ * Clear the key pair (logout)
789
+ */
790
+ async clear() {
791
+ await this.storage.clear();
792
+ this.keyPair = null;
793
+ }
794
+ /**
795
+ * Check if a key pair exists
796
+ */
797
+ async hasKeyPair() {
798
+ if (this.keyPair) return true;
799
+ const loaded = await this.storage.load();
800
+ return loaded !== null;
801
+ }
802
+ /**
803
+ * Migrate key pair to a new storage backend
804
+ *
805
+ * This preserves the existing key pair while switching storage.
806
+ * The key is saved to the new storage and removed from the old storage.
807
+ *
808
+ * @param newStorage - The new storage backend to migrate to
809
+ */
810
+ async migrateStorage(newStorage) {
811
+ const currentKeyPair = this.keyPair ?? await this.storage.load();
812
+ await this.storage.clear();
813
+ this.storage = newStorage;
814
+ if (currentKeyPair) {
815
+ await this.storage.save(currentKeyPair);
816
+ this.keyPair = currentKeyPair;
817
+ }
818
+ }
819
+ /**
820
+ * Get the current storage backend
821
+ */
822
+ getStorage() {
823
+ return this.storage;
824
+ }
825
+ };
826
+
827
+ // src/secure-client.ts
828
+ var SecureKentuckySignerClient = class {
829
+ constructor(options) {
830
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
831
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
832
+ this.timeout = options.timeout ?? 3e4;
833
+ this.keyManager = options.ephemeralKeyManager ?? new EphemeralKeyManager(
834
+ options.ephemeralKeyStorage ?? new MemoryEphemeralKeyStorage()
835
+ );
836
+ this.requireSigning = options.requireEphemeralSigning ?? true;
837
+ }
838
+ /**
839
+ * Get the ephemeral key manager for advanced usage
840
+ */
841
+ getKeyManager() {
842
+ return this.keyManager;
843
+ }
844
+ /**
845
+ * Make a signed request to the API
846
+ */
847
+ async request(path, options = {}) {
848
+ const { token, skipSigning, ...fetchOptions } = options;
849
+ const headers = {
850
+ "Content-Type": "application/json",
851
+ ...options.headers
852
+ };
853
+ if (token) {
854
+ ;
855
+ headers["Authorization"] = `Bearer ${token}`;
856
+ }
857
+ let body = options.body;
858
+ if (token && !skipSigning && this.requireSigning && body) {
859
+ const signedPayload = await this.keyManager.signPayload(body);
860
+ headers["X-Ephemeral-Signature"] = signedPayload.signature;
861
+ headers["X-Ephemeral-Timestamp"] = signedPayload.timestamp.toString();
862
+ }
863
+ const controller = new AbortController();
864
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
865
+ try {
866
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
867
+ ...fetchOptions,
868
+ headers,
869
+ body,
870
+ signal: controller.signal
871
+ });
872
+ const data = await response.json();
873
+ if (!response.ok || data.success === false) {
874
+ const error = data;
875
+ throw new KentuckySignerError(
876
+ error.error?.message ?? "Unknown error",
877
+ error.error?.code ?? "UNKNOWN_ERROR",
878
+ error.error?.details
879
+ );
880
+ }
881
+ return data;
882
+ } finally {
883
+ clearTimeout(timeoutId);
884
+ }
885
+ }
886
+ /**
887
+ * Get a challenge for passkey authentication
888
+ */
889
+ async getChallenge(accountId) {
890
+ return this.request("/api/auth/challenge", {
891
+ method: "POST",
892
+ body: JSON.stringify({ account_id: accountId }),
893
+ skipSigning: true
894
+ // No token yet
895
+ });
896
+ }
897
+ /**
898
+ * Authenticate with a passkey credential
899
+ *
900
+ * Automatically sends the ephemeral public key for binding.
901
+ */
80
902
  async authenticatePasskey(accountId, credential) {
903
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
81
904
  return this.request("/api/auth/passkey", {
82
905
  method: "POST",
83
906
  body: JSON.stringify({
@@ -86,70 +909,86 @@ var KentuckySignerClient = class {
86
909
  client_data_json: credential.clientDataJSON,
87
910
  authenticator_data: credential.authenticatorData,
88
911
  signature: credential.signature,
89
- user_handle: credential.userHandle
90
- })
912
+ user_handle: credential.userHandle,
913
+ ephemeral_public_key: ephemeralPublicKey
914
+ }),
915
+ skipSigning: true
916
+ // No token yet
917
+ });
918
+ }
919
+ /**
920
+ * Authenticate with password
921
+ *
922
+ * Automatically sends the ephemeral public key for binding.
923
+ */
924
+ async authenticatePassword(request) {
925
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
926
+ return this.request("/api/auth/password", {
927
+ method: "POST",
928
+ body: JSON.stringify({
929
+ ...request,
930
+ ephemeral_public_key: ephemeralPublicKey
931
+ }),
932
+ skipSigning: true
933
+ // No token yet
91
934
  });
92
935
  }
93
936
  /**
94
937
  * Refresh an authentication token
95
938
  *
96
- * @param token - Current JWT token
97
- * @returns New authentication response with fresh token
939
+ * Generates a new ephemeral key pair and binds it to the new token.
98
940
  */
99
941
  async refreshToken(token) {
942
+ await this.keyManager.rotate();
943
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
100
944
  return this.request("/api/auth/refresh", {
101
945
  method: "POST",
102
- token
946
+ token,
947
+ body: JSON.stringify({
948
+ ephemeral_public_key: ephemeralPublicKey
949
+ }),
950
+ skipSigning: true
951
+ // Use old token, but send new key
103
952
  });
104
953
  }
105
954
  /**
106
955
  * Logout and invalidate token
107
956
  *
108
- * @param token - JWT token to invalidate
957
+ * Clears the ephemeral key pair.
109
958
  */
110
959
  async logout(token) {
111
960
  await this.request("/api/auth/logout", {
112
961
  method: "POST",
113
- token
962
+ token,
963
+ skipSigning: true
114
964
  });
965
+ await this.keyManager.clear();
115
966
  }
116
967
  /**
117
- * Get account information
118
- *
119
- * @param accountId - Account ID
120
- * @param token - JWT token
121
- * @returns Account info with addresses and passkeys
968
+ * Get account information (signed request)
122
969
  */
123
970
  async getAccountInfo(accountId, token) {
124
971
  return this.request(`/api/accounts/${accountId}`, {
125
972
  method: "GET",
126
- token
973
+ token,
974
+ skipSigning: true
127
975
  });
128
976
  }
129
977
  /**
130
- * Check if an account exists
131
- *
132
- * @param accountId - Account ID
133
- * @param token - JWT token
134
- * @returns True if account exists
978
+ * Get extended account information (signed request)
135
979
  */
136
- async accountExists(accountId, token) {
137
- try {
138
- await this.request(`/api/accounts/${accountId}`, {
139
- method: "HEAD",
140
- token
141
- });
142
- return true;
143
- } catch {
144
- return false;
145
- }
980
+ async getAccountInfoExtended(accountId, token) {
981
+ return this.request(
982
+ `/api/accounts/${accountId}`,
983
+ {
984
+ method: "GET",
985
+ token,
986
+ skipSigning: true
987
+ }
988
+ );
146
989
  }
147
990
  /**
148
- * Sign an EVM transaction hash
149
- *
150
- * @param request - Sign request with tx_hash and chain_id
151
- * @param token - JWT token
152
- * @returns Signature response with r, s, v components
991
+ * Sign an EVM transaction hash (signed request)
153
992
  */
154
993
  async signEvmTransaction(request, token) {
155
994
  return this.request("/api/sign/evm", {
@@ -159,14 +998,17 @@ var KentuckySignerClient = class {
159
998
  });
160
999
  }
161
1000
  /**
162
- * Sign a raw hash for EVM
163
- *
164
- * Convenience method that wraps signEvmTransaction.
165
- *
166
- * @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
167
- * @param chainId - Chain ID
168
- * @param token - JWT token
169
- * @returns Full signature (hex encoded with 0x prefix)
1001
+ * Sign an EVM transaction hash with 2FA (signed request)
1002
+ */
1003
+ async signEvmTransactionWith2FA(request, token) {
1004
+ return this.request("/api/sign/evm", {
1005
+ method: "POST",
1006
+ token,
1007
+ body: JSON.stringify(request)
1008
+ });
1009
+ }
1010
+ /**
1011
+ * Sign a raw hash for EVM (signed request)
170
1012
  */
171
1013
  async signHash(hash, chainId, token) {
172
1014
  const response = await this.signEvmTransaction(
@@ -176,101 +1018,86 @@ var KentuckySignerClient = class {
176
1018
  return response.signature.full;
177
1019
  }
178
1020
  /**
179
- * Create a new account with passkey authentication
180
- *
181
- * @param credential - WebAuthn credential from navigator.credentials.create()
182
- * @returns Account creation response with account ID and addresses
1021
+ * Add password to account (signed request)
183
1022
  */
184
- async createAccountWithPasskey(credential) {
185
- return this.request("/api/accounts/create/passkey", {
186
- method: "POST",
187
- body: JSON.stringify({
188
- credential_id: credential.credentialId,
189
- public_key: credential.publicKey,
190
- client_data_json: credential.clientDataJSON,
191
- authenticator_data: credential.authenticatorData
192
- })
193
- });
1023
+ async addPassword(accountId, request, token) {
1024
+ return this.request(
1025
+ `/api/accounts/${accountId}/password`,
1026
+ {
1027
+ method: "POST",
1028
+ token,
1029
+ body: JSON.stringify(request)
1030
+ }
1031
+ );
194
1032
  }
195
1033
  /**
196
- * Create a new account with password authentication
197
- *
198
- * @param request - Password and confirmation
199
- * @returns Account creation response with account ID and addresses
1034
+ * Add passkey to account (signed request)
200
1035
  */
201
- async createAccountWithPassword(request) {
202
- return this.request("/api/accounts/create/password", {
1036
+ async addPasskey(accountId, request, token) {
1037
+ return this.request(
1038
+ `/api/accounts/${accountId}/passkeys`,
1039
+ {
1040
+ method: "POST",
1041
+ token,
1042
+ body: JSON.stringify(request)
1043
+ }
1044
+ );
1045
+ }
1046
+ /**
1047
+ * Remove passkey from account (signed request)
1048
+ */
1049
+ async removePasskey(accountId, passkeyIndex, token) {
1050
+ return this.request(
1051
+ `/api/accounts/${accountId}/passkeys/${passkeyIndex}`,
1052
+ {
1053
+ method: "DELETE",
1054
+ token,
1055
+ body: JSON.stringify({ passkey_index: passkeyIndex })
1056
+ }
1057
+ );
1058
+ }
1059
+ /**
1060
+ * Create account with passkey (public endpoint, no signing)
1061
+ */
1062
+ async createAccountWithPasskey(attestationObject, label) {
1063
+ const body = {
1064
+ attestation_object: attestationObject
1065
+ };
1066
+ if (label) {
1067
+ body.label = label;
1068
+ }
1069
+ return this.request("/api/accounts/create/passkey", {
203
1070
  method: "POST",
204
- body: JSON.stringify(request)
1071
+ body: JSON.stringify(body),
1072
+ skipSigning: true
205
1073
  });
206
1074
  }
207
1075
  /**
208
- * Authenticate with password
209
- *
210
- * @param request - Account ID and password
211
- * @returns Authentication response with JWT token
1076
+ * Create account with password (public endpoint, no signing)
212
1077
  */
213
- async authenticatePassword(request) {
214
- return this.request("/api/auth/password", {
1078
+ async createAccountWithPassword(request) {
1079
+ return this.request("/api/accounts/create/password", {
215
1080
  method: "POST",
216
- body: JSON.stringify(request)
1081
+ body: JSON.stringify(request),
1082
+ skipSigning: true
217
1083
  });
218
1084
  }
219
1085
  /**
220
- * Health check
221
- *
222
- * @returns True if the API is healthy
1086
+ * Health check (public endpoint)
223
1087
  */
224
1088
  async healthCheck() {
225
1089
  try {
226
1090
  const response = await this.request("/api/health", {
227
- method: "GET"
1091
+ method: "GET",
1092
+ skipSigning: true
228
1093
  });
229
1094
  return response.status === "ok";
230
1095
  } catch {
231
1096
  return false;
232
1097
  }
233
1098
  }
234
- /**
235
- * Get API version
236
- *
237
- * @returns Version string
238
- */
239
- async getVersion() {
240
- const response = await this.request("/api/version", {
241
- method: "GET"
242
- });
243
- return response.version;
244
- }
245
1099
  };
246
1100
 
247
- // src/utils.ts
248
- function base64UrlEncode(data) {
249
- let base64;
250
- if (typeof Buffer !== "undefined") {
251
- base64 = Buffer.from(data).toString("base64");
252
- } else {
253
- const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
254
- base64 = btoa(binary);
255
- }
256
- return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
257
- }
258
- function base64UrlDecode(str) {
259
- let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
260
- const padding = (4 - base64.length % 4) % 4;
261
- base64 += "=".repeat(padding);
262
- if (typeof Buffer !== "undefined") {
263
- return new Uint8Array(Buffer.from(base64, "base64"));
264
- } else {
265
- const binary = atob(base64);
266
- const bytes = new Uint8Array(binary.length);
267
- for (let i = 0; i < binary.length; i++) {
268
- bytes[i] = binary.charCodeAt(i);
269
- }
270
- return bytes;
271
- }
272
- }
273
-
274
1101
  // src/auth.ts
275
1102
  function isWebAuthnAvailable() {
276
1103
  return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
@@ -345,7 +1172,8 @@ async function authenticateWithPasskey(options) {
345
1172
  const passkeyCredential = credentialToPasskey(credential);
346
1173
  const authResponse = await client.authenticatePasskey(
347
1174
  options.accountId,
348
- passkeyCredential
1175
+ passkeyCredential,
1176
+ options.ephemeralPublicKey
349
1177
  );
350
1178
  const accountInfo = await client.getAccountInfo(
351
1179
  options.accountId,
@@ -376,6 +1204,30 @@ async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
376
1204
  expiresAt: Date.now() + authResponse.expires_in * 1e3
377
1205
  };
378
1206
  }
1207
+ async function authenticateWithPassword(options) {
1208
+ const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
1209
+ const authRequest = {
1210
+ account_id: options.accountId,
1211
+ password: options.password
1212
+ };
1213
+ if (options.ephemeralPublicKey) {
1214
+ authRequest.ephemeral_public_key = options.ephemeralPublicKey;
1215
+ }
1216
+ const authResponse = await client.authenticatePassword(authRequest);
1217
+ const accountInfo = await client.getAccountInfo(
1218
+ options.accountId,
1219
+ authResponse.token
1220
+ );
1221
+ const expiresAt = Date.now() + authResponse.expires_in * 1e3;
1222
+ return {
1223
+ token: authResponse.token,
1224
+ accountId: options.accountId,
1225
+ evmAddress: accountInfo.addresses.evm,
1226
+ btcAddress: accountInfo.addresses.bitcoin,
1227
+ solAddress: accountInfo.addresses.solana,
1228
+ expiresAt
1229
+ };
1230
+ }
379
1231
 
380
1232
  // src/account.ts
381
1233
  import {
@@ -386,9 +1238,9 @@ import {
386
1238
  } from "viem";
387
1239
  import { toAccount } from "viem/accounts";
388
1240
  function createKentuckySignerAccount(options) {
389
- const { config, defaultChainId = 1, onSessionExpired } = options;
1241
+ const { config, defaultChainId = 1, onSessionExpired, secureClient, on2FARequired } = options;
390
1242
  let session = options.session;
391
- const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
1243
+ const client = secureClient ?? new KentuckySignerClient({ baseUrl: config.baseUrl });
392
1244
  async function getToken() {
393
1245
  if (Date.now() + 6e4 >= session.expiresAt) {
394
1246
  if (onSessionExpired) {
@@ -405,17 +1257,63 @@ function createKentuckySignerAccount(options) {
405
1257
  }
406
1258
  async function signHash(hash, chainId) {
407
1259
  const token = await getToken();
408
- const response = await client.signEvmTransaction(
409
- { tx_hash: hash, chain_id: chainId },
410
- token
411
- );
412
- return response.signature.full;
1260
+ try {
1261
+ const response = await client.signEvmTransaction(
1262
+ { tx_hash: hash, chain_id: chainId },
1263
+ token
1264
+ );
1265
+ return response.signature.full;
1266
+ } catch (err) {
1267
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
1268
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
1269
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
1270
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
1271
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
1272
+ if (!codes) {
1273
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
1274
+ }
1275
+ const response = await client.signEvmTransactionWith2FA(
1276
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
1277
+ token
1278
+ );
1279
+ return response.signature.full;
1280
+ }
1281
+ throw err;
1282
+ }
413
1283
  }
414
- function parseSignature(signature) {
415
- const r = `0x${signature.slice(2, 66)}`;
416
- const s = `0x${signature.slice(66, 130)}`;
417
- const v = BigInt(`0x${signature.slice(130, 132)}`);
418
- return { r, s, v };
1284
+ async function signHashWithComponents(hash, chainId) {
1285
+ const token = await getToken();
1286
+ try {
1287
+ const response = await client.signEvmTransaction(
1288
+ { tx_hash: hash, chain_id: chainId },
1289
+ token
1290
+ );
1291
+ return {
1292
+ r: response.signature.r,
1293
+ s: response.signature.s,
1294
+ v: response.signature.v
1295
+ };
1296
+ } catch (err) {
1297
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
1298
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
1299
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
1300
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
1301
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
1302
+ if (!codes) {
1303
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
1304
+ }
1305
+ const response = await client.signEvmTransactionWith2FA(
1306
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
1307
+ token
1308
+ );
1309
+ return {
1310
+ r: response.signature.r,
1311
+ s: response.signature.s,
1312
+ v: response.signature.v
1313
+ };
1314
+ }
1315
+ throw err;
1316
+ }
419
1317
  }
420
1318
  const account = toAccount({
421
1319
  address: session.evmAddress,
@@ -438,13 +1336,12 @@ function createKentuckySignerAccount(options) {
438
1336
  const chainId = transaction.chainId ?? defaultChainId;
439
1337
  const serializedUnsigned = serializeTransaction(transaction);
440
1338
  const txHash = keccak256(serializedUnsigned);
441
- const signature = await signHash(txHash, chainId);
442
- const { r, s, v } = parseSignature(signature);
1339
+ const { r, s, v } = await signHashWithComponents(txHash, chainId);
443
1340
  let yParity;
444
1341
  if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
445
- yParity = Number(v) - 27;
1342
+ yParity = v >= 27 ? v - 27 : v;
446
1343
  } else {
447
- yParity = Number(v);
1344
+ yParity = v;
448
1345
  }
449
1346
  const serializedSigned = serializeTransaction(transaction, {
450
1347
  r,
@@ -483,29 +1380,91 @@ function KentuckySignerProvider({
483
1380
  defaultChainId = 1,
484
1381
  storageKeyPrefix = "kentucky_signer",
485
1382
  persistSession = true,
1383
+ useEphemeralKeys = false,
486
1384
  children
487
1385
  }) {
1386
+ const getInitialPersistSetting = () => {
1387
+ if (typeof localStorage !== "undefined") {
1388
+ const saved = localStorage.getItem(`${storageKeyPrefix}_persist_ephemeral_keys`);
1389
+ if (saved !== null) {
1390
+ return saved === "true";
1391
+ }
1392
+ }
1393
+ return typeof indexedDB !== "undefined";
1394
+ };
1395
+ const getInitialSecureModeSetting = () => {
1396
+ if (typeof localStorage !== "undefined") {
1397
+ const saved = localStorage.getItem(`${storageKeyPrefix}_secure_mode`);
1398
+ if (saved !== null) {
1399
+ return saved === "true";
1400
+ }
1401
+ }
1402
+ return useEphemeralKeys;
1403
+ };
1404
+ const defaultTwoFactorPrompt = {
1405
+ isVisible: false,
1406
+ totpRequired: false,
1407
+ pinRequired: false,
1408
+ pinLength: 6
1409
+ };
488
1410
  const [state, setState] = useState({
489
1411
  isAuthenticating: false,
490
1412
  isAuthenticated: false,
491
1413
  session: null,
492
1414
  account: null,
493
- error: null
1415
+ error: null,
1416
+ ephemeralKeyBound: false,
1417
+ secureMode: getInitialSecureModeSetting(),
1418
+ persistEphemeralKeys: getInitialPersistSetting(),
1419
+ twoFactorPrompt: defaultTwoFactorPrompt
494
1420
  });
1421
+ const indexedDBStorage = useMemo(
1422
+ () => typeof indexedDB !== "undefined" ? new IndexedDBEphemeralKeyStorage() : null,
1423
+ []
1424
+ );
1425
+ const memoryStorage = useMemo(() => new MemoryEphemeralKeyStorage(), []);
1426
+ const ephemeralKeyStorage = state.persistEphemeralKeys && indexedDBStorage ? indexedDBStorage : memoryStorage;
1427
+ const ephemeralKeyManagerRef = useRef(null);
1428
+ if (!ephemeralKeyManagerRef.current) {
1429
+ ephemeralKeyManagerRef.current = new EphemeralKeyManager(ephemeralKeyStorage);
1430
+ }
1431
+ const ephemeralKeyManager = ephemeralKeyManagerRef.current;
495
1432
  const client = useMemo(
496
1433
  () => new KentuckySignerClient({ baseUrl }),
497
1434
  [baseUrl]
498
1435
  );
1436
+ const secureClient = useMemo(
1437
+ () => new SecureKentuckySignerClient({
1438
+ baseUrl,
1439
+ ephemeralKeyManager
1440
+ }),
1441
+ [baseUrl, ephemeralKeyManager]
1442
+ );
499
1443
  const storage = useMemo(
500
1444
  () => persistSession ? new LocalStorageTokenStorage(storageKeyPrefix) : null,
501
1445
  [persistSession, storageKeyPrefix]
502
1446
  );
1447
+ const handle2FARequired = useCallback(async (requirements) => {
1448
+ return new Promise((resolve) => {
1449
+ setState((s) => ({
1450
+ ...s,
1451
+ twoFactorPrompt: {
1452
+ isVisible: true,
1453
+ totpRequired: requirements.totpRequired,
1454
+ pinRequired: requirements.pinRequired,
1455
+ pinLength: requirements.pinLength,
1456
+ resolve
1457
+ }
1458
+ }));
1459
+ });
1460
+ }, []);
503
1461
  const createAccount = useCallback(
504
- (session) => {
1462
+ (session, useSecureClient = false) => {
505
1463
  return createKentuckySignerAccount({
506
1464
  config: { baseUrl, accountId: session.accountId },
507
1465
  session,
508
1466
  defaultChainId,
1467
+ secureClient: useSecureClient ? secureClient : void 0,
509
1468
  onSessionExpired: async () => {
510
1469
  const newSession = await refreshSessionIfNeeded(session, baseUrl, 0);
511
1470
  setState((s) => ({
@@ -514,10 +1473,11 @@ function KentuckySignerProvider({
514
1473
  account: s.account ? { ...s.account, session: newSession } : null
515
1474
  }));
516
1475
  return newSession;
517
- }
1476
+ },
1477
+ on2FARequired: handle2FARequired
518
1478
  });
519
1479
  },
520
- [baseUrl, defaultChainId]
1480
+ [baseUrl, defaultChainId, secureClient, handle2FARequired]
521
1481
  );
522
1482
  useEffect(() => {
523
1483
  async function restoreSession() {
@@ -529,30 +1489,42 @@ function KentuckySignerProvider({
529
1489
  if (!isSessionValid(session)) {
530
1490
  try {
531
1491
  const refreshed = await refreshSessionIfNeeded(session, baseUrl, 0);
532
- const account2 = createAccount(refreshed);
533
1492
  localStorage.setItem(
534
1493
  `${storageKeyPrefix}_session`,
535
1494
  JSON.stringify(refreshed)
536
1495
  );
537
- setState({
538
- isAuthenticating: false,
539
- isAuthenticated: true,
540
- session: refreshed,
541
- account: account2,
542
- error: null
1496
+ setState((s) => {
1497
+ const account = createAccount(refreshed, s.secureMode);
1498
+ return {
1499
+ isAuthenticating: false,
1500
+ isAuthenticated: true,
1501
+ session: refreshed,
1502
+ account,
1503
+ error: null,
1504
+ ephemeralKeyBound: false,
1505
+ secureMode: s.secureMode,
1506
+ persistEphemeralKeys: s.persistEphemeralKeys,
1507
+ twoFactorPrompt: s.twoFactorPrompt
1508
+ };
543
1509
  });
544
1510
  } catch {
545
1511
  localStorage.removeItem(`${storageKeyPrefix}_session`);
546
1512
  }
547
1513
  return;
548
1514
  }
549
- const account = createAccount(session);
550
- setState({
551
- isAuthenticating: false,
552
- isAuthenticated: true,
553
- session,
554
- account,
555
- error: null
1515
+ setState((s) => {
1516
+ const account = createAccount(session, s.secureMode);
1517
+ return {
1518
+ isAuthenticating: false,
1519
+ isAuthenticated: true,
1520
+ session,
1521
+ account,
1522
+ error: null,
1523
+ ephemeralKeyBound: false,
1524
+ secureMode: s.secureMode,
1525
+ persistEphemeralKeys: s.persistEphemeralKeys,
1526
+ twoFactorPrompt: s.twoFactorPrompt
1527
+ };
556
1528
  });
557
1529
  } catch {
558
1530
  localStorage.removeItem(`${storageKeyPrefix}_session`);
@@ -564,24 +1536,42 @@ function KentuckySignerProvider({
564
1536
  async (accountId, options) => {
565
1537
  setState((s) => ({ ...s, isAuthenticating: true, error: null }));
566
1538
  try {
567
- const session = await authenticateWithPasskey({
568
- baseUrl,
569
- accountId,
570
- rpId: options?.rpId
571
- });
572
- const account = createAccount(session);
1539
+ let session;
1540
+ let ephemeralKeyBound = false;
1541
+ if (options?.session) {
1542
+ session = options.session;
1543
+ } else {
1544
+ let ephemeralPublicKey;
1545
+ if (state.secureMode) {
1546
+ ephemeralPublicKey = await ephemeralKeyManager.getPublicKey();
1547
+ }
1548
+ session = await authenticateWithPasskey({
1549
+ baseUrl,
1550
+ accountId,
1551
+ rpId: options?.rpId,
1552
+ ephemeralPublicKey
1553
+ });
1554
+ ephemeralKeyBound = !!ephemeralPublicKey;
1555
+ }
573
1556
  if (storage) {
574
1557
  localStorage.setItem(
575
1558
  `${storageKeyPrefix}_session`,
576
1559
  JSON.stringify(session)
577
1560
  );
578
1561
  }
579
- setState({
580
- isAuthenticating: false,
581
- isAuthenticated: true,
582
- session,
583
- account,
584
- error: null
1562
+ setState((s) => {
1563
+ const account = createAccount(session, s.secureMode);
1564
+ return {
1565
+ isAuthenticating: false,
1566
+ isAuthenticated: true,
1567
+ session,
1568
+ account,
1569
+ error: null,
1570
+ ephemeralKeyBound,
1571
+ secureMode: s.secureMode,
1572
+ persistEphemeralKeys: s.persistEphemeralKeys,
1573
+ twoFactorPrompt: s.twoFactorPrompt
1574
+ };
585
1575
  });
586
1576
  } catch (error) {
587
1577
  setState((s) => ({
@@ -592,7 +1582,7 @@ function KentuckySignerProvider({
592
1582
  throw error;
593
1583
  }
594
1584
  },
595
- [baseUrl, createAccount, storage, storageKeyPrefix]
1585
+ [baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
596
1586
  );
597
1587
  const logout = useCallback(async () => {
598
1588
  try {
@@ -604,31 +1594,40 @@ function KentuckySignerProvider({
604
1594
  if (storage) {
605
1595
  localStorage.removeItem(`${storageKeyPrefix}_session`);
606
1596
  }
607
- setState({
1597
+ if (ephemeralKeyManager) {
1598
+ await ephemeralKeyManager.clear();
1599
+ }
1600
+ setState((s) => ({
608
1601
  isAuthenticating: false,
609
1602
  isAuthenticated: false,
610
1603
  session: null,
611
1604
  account: null,
612
- error: null
613
- });
614
- }, [client, state.session, storage, storageKeyPrefix]);
1605
+ error: null,
1606
+ ephemeralKeyBound: false,
1607
+ secureMode: s.secureMode,
1608
+ persistEphemeralKeys: s.persistEphemeralKeys,
1609
+ twoFactorPrompt: defaultTwoFactorPrompt
1610
+ }));
1611
+ }, [client, state.session, storage, storageKeyPrefix, ephemeralKeyManager]);
615
1612
  const refreshSession = useCallback(async () => {
616
1613
  if (!state.session) return;
617
1614
  try {
618
1615
  const refreshed = await refreshSessionIfNeeded(state.session, baseUrl);
619
1616
  if (refreshed !== state.session) {
620
- const account = createAccount(refreshed);
621
1617
  if (storage) {
622
1618
  localStorage.setItem(
623
1619
  `${storageKeyPrefix}_session`,
624
1620
  JSON.stringify(refreshed)
625
1621
  );
626
1622
  }
627
- setState((s) => ({
628
- ...s,
629
- session: refreshed,
630
- account
631
- }));
1623
+ setState((s) => {
1624
+ const account = createAccount(refreshed, s.secureMode);
1625
+ return {
1626
+ ...s,
1627
+ session: refreshed,
1628
+ account
1629
+ };
1630
+ });
632
1631
  }
633
1632
  } catch (error) {
634
1633
  setState((s) => ({ ...s, error }));
@@ -637,15 +1636,119 @@ function KentuckySignerProvider({
637
1636
  const clearError = useCallback(() => {
638
1637
  setState((s) => ({ ...s, error: null }));
639
1638
  }, []);
1639
+ const setSecureMode = useCallback((enabled) => {
1640
+ if (typeof localStorage !== "undefined") {
1641
+ localStorage.setItem(`${storageKeyPrefix}_secure_mode`, String(enabled));
1642
+ }
1643
+ setState((s) => {
1644
+ const newAccount = s.session ? createAccount(s.session, enabled) : null;
1645
+ return {
1646
+ ...s,
1647
+ secureMode: enabled,
1648
+ account: newAccount
1649
+ };
1650
+ });
1651
+ }, [createAccount, storageKeyPrefix]);
1652
+ const setPersistEphemeralKeys = useCallback(async (enabled) => {
1653
+ const newStorage = enabled && indexedDBStorage ? indexedDBStorage : memoryStorage;
1654
+ await ephemeralKeyManager.migrateStorage(newStorage);
1655
+ if (typeof localStorage !== "undefined") {
1656
+ localStorage.setItem(`${storageKeyPrefix}_persist_ephemeral_keys`, String(enabled));
1657
+ }
1658
+ setState((s) => ({
1659
+ ...s,
1660
+ persistEphemeralKeys: enabled
1661
+ }));
1662
+ }, [ephemeralKeyManager, storageKeyPrefix, indexedDBStorage, memoryStorage]);
1663
+ const getEphemeralPublicKey = useCallback(async () => {
1664
+ if (!state.secureMode) {
1665
+ return void 0;
1666
+ }
1667
+ return ephemeralKeyManager.getPublicKey();
1668
+ }, [state.secureMode, ephemeralKeyManager]);
1669
+ const authenticatePassword = useCallback(
1670
+ async (accountId, password) => {
1671
+ setState((s) => ({ ...s, isAuthenticating: true, error: null }));
1672
+ try {
1673
+ let ephemeralPublicKey;
1674
+ console.log("[KentuckySigner] authenticatePassword - secureMode:", state.secureMode);
1675
+ if (state.secureMode) {
1676
+ ephemeralPublicKey = await ephemeralKeyManager.getPublicKey();
1677
+ console.log("[KentuckySigner] Got ephemeral public key for binding:", ephemeralPublicKey?.substring(0, 50) + "...");
1678
+ }
1679
+ const session = await authenticateWithPassword({
1680
+ baseUrl,
1681
+ accountId,
1682
+ password,
1683
+ ephemeralPublicKey
1684
+ });
1685
+ const ephemeralKeyBound = !!ephemeralPublicKey;
1686
+ if (storage) {
1687
+ localStorage.setItem(
1688
+ `${storageKeyPrefix}_session`,
1689
+ JSON.stringify(session)
1690
+ );
1691
+ }
1692
+ setState((s) => {
1693
+ const account = createAccount(session, s.secureMode);
1694
+ return {
1695
+ isAuthenticating: false,
1696
+ isAuthenticated: true,
1697
+ session,
1698
+ account,
1699
+ error: null,
1700
+ ephemeralKeyBound,
1701
+ secureMode: s.secureMode,
1702
+ persistEphemeralKeys: s.persistEphemeralKeys,
1703
+ twoFactorPrompt: s.twoFactorPrompt
1704
+ };
1705
+ });
1706
+ } catch (error) {
1707
+ setState((s) => ({
1708
+ ...s,
1709
+ isAuthenticating: false,
1710
+ error
1711
+ }));
1712
+ throw error;
1713
+ }
1714
+ },
1715
+ [baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
1716
+ );
1717
+ const submit2FA = useCallback((codes) => {
1718
+ const { resolve } = state.twoFactorPrompt;
1719
+ if (resolve) {
1720
+ resolve(codes);
1721
+ }
1722
+ setState((s) => ({
1723
+ ...s,
1724
+ twoFactorPrompt: defaultTwoFactorPrompt
1725
+ }));
1726
+ }, [state.twoFactorPrompt]);
1727
+ const cancel2FA = useCallback(() => {
1728
+ const { resolve } = state.twoFactorPrompt;
1729
+ if (resolve) {
1730
+ resolve(null);
1731
+ }
1732
+ setState((s) => ({
1733
+ ...s,
1734
+ twoFactorPrompt: defaultTwoFactorPrompt
1735
+ }));
1736
+ }, [state.twoFactorPrompt]);
640
1737
  const value = useMemo(
641
1738
  () => ({
642
1739
  ...state,
643
1740
  authenticate,
1741
+ authenticatePassword,
644
1742
  logout,
645
1743
  refreshSession,
646
- clearError
1744
+ clearError,
1745
+ setSecureMode,
1746
+ setPersistEphemeralKeys,
1747
+ getEphemeralPublicKey,
1748
+ submit2FA,
1749
+ cancel2FA
647
1750
  }),
648
- [state, authenticate, logout, refreshSession, clearError]
1751
+ [state, authenticate, authenticatePassword, logout, refreshSession, clearError, setSecureMode, setPersistEphemeralKeys, getEphemeralPublicKey, submit2FA, cancel2FA]
649
1752
  );
650
1753
  return /* @__PURE__ */ jsx(KentuckySignerContext.Provider, { value, children });
651
1754
  }
@@ -670,10 +1773,21 @@ function useKentuckySigner() {
670
1773
  session: context.session,
671
1774
  account: context.account,
672
1775
  error: context.error,
1776
+ ephemeralKeyBound: context.ephemeralKeyBound,
673
1777
  authenticate: context.authenticate,
1778
+ authenticatePassword: context.authenticatePassword,
674
1779
  logout: context.logout,
675
1780
  refreshSession: context.refreshSession,
676
- clearError: context.clearError
1781
+ clearError: context.clearError,
1782
+ secureMode: context.secureMode,
1783
+ setSecureMode: context.setSecureMode,
1784
+ persistEphemeralKeys: context.persistEphemeralKeys,
1785
+ setPersistEphemeralKeys: context.setPersistEphemeralKeys,
1786
+ getEphemeralPublicKey: context.getEphemeralPublicKey,
1787
+ // 2FA support
1788
+ twoFactorPrompt: context.twoFactorPrompt,
1789
+ submit2FA: context.submit2FA,
1790
+ cancel2FA: context.cancel2FA
677
1791
  };
678
1792
  }
679
1793
  function useKentuckySignerAccount() {