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