kentucky-signer-viem 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -19,7 +19,7 @@ var KentuckySignerError = class extends Error {
19
19
  var KentuckySignerClient = class {
20
20
  constructor(options) {
21
21
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
22
- this.fetchImpl = options.fetch ?? globalThis.fetch;
22
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
23
23
  this.timeout = options.timeout ?? 3e4;
24
24
  }
25
25
  /**
@@ -74,19 +74,24 @@ var KentuckySignerClient = class {
74
74
  *
75
75
  * @param accountId - Account ID to authenticate
76
76
  * @param credential - WebAuthn credential from navigator.credentials.get()
77
+ * @param ephemeralPublicKey - Optional ephemeral public key for secure mode binding
77
78
  * @returns Authentication response with JWT token
78
79
  */
79
- async authenticatePasskey(accountId, credential) {
80
+ async authenticatePasskey(accountId, credential, ephemeralPublicKey) {
81
+ const body = {
82
+ account_id: accountId,
83
+ credential_id: credential.credentialId,
84
+ client_data_json: credential.clientDataJSON,
85
+ authenticator_data: credential.authenticatorData,
86
+ signature: credential.signature,
87
+ user_handle: credential.userHandle
88
+ };
89
+ if (ephemeralPublicKey) {
90
+ body.ephemeral_public_key = ephemeralPublicKey;
91
+ }
80
92
  return this.request("/api/auth/passkey", {
81
93
  method: "POST",
82
- body: JSON.stringify({
83
- account_id: accountId,
84
- credential_id: credential.credentialId,
85
- client_data_json: credential.clientDataJSON,
86
- authenticator_data: credential.authenticatorData,
87
- signature: credential.signature,
88
- user_handle: credential.userHandle
89
- })
94
+ body: JSON.stringify(body)
90
95
  });
91
96
  }
92
97
  /**
@@ -177,18 +182,20 @@ var KentuckySignerClient = class {
177
182
  /**
178
183
  * Create a new account with passkey authentication
179
184
  *
180
- * @param credential - WebAuthn credential from navigator.credentials.create()
185
+ * @param attestationObject - Base64url encoded attestation object from WebAuthn
186
+ * @param label - Optional label for the passkey (defaults to "Owner Passkey")
181
187
  * @returns Account creation response with account ID and addresses
182
188
  */
183
- async createAccountWithPasskey(credential) {
189
+ async createAccountWithPasskey(attestationObject, label) {
190
+ const body = {
191
+ attestation_object: attestationObject
192
+ };
193
+ if (label) {
194
+ body.label = label;
195
+ }
184
196
  return this.request("/api/accounts/create/passkey", {
185
197
  method: "POST",
186
- body: JSON.stringify({
187
- credential_id: credential.credentialId,
188
- public_key: credential.publicKey,
189
- client_data_json: credential.clientDataJSON,
190
- authenticator_data: credential.authenticatorData
191
- })
198
+ body: JSON.stringify(body)
192
199
  });
193
200
  }
194
201
  /**
@@ -215,6 +222,68 @@ var KentuckySignerClient = class {
215
222
  body: JSON.stringify(request)
216
223
  });
217
224
  }
225
+ /**
226
+ * Add password authentication to an existing account
227
+ *
228
+ * Enables password-based authentication for an account that was created
229
+ * with passkey-only authentication.
230
+ *
231
+ * @param accountId - Account ID
232
+ * @param request - Password and confirmation
233
+ * @param token - JWT token
234
+ * @returns Success response
235
+ */
236
+ async addPassword(accountId, request, token) {
237
+ return this.request(`/api/accounts/${accountId}/password`, {
238
+ method: "POST",
239
+ token,
240
+ body: JSON.stringify(request)
241
+ });
242
+ }
243
+ /**
244
+ * Get extended account information including auth config
245
+ *
246
+ * @param accountId - Account ID
247
+ * @param token - JWT token
248
+ * @returns Extended account info with auth config and passkey count
249
+ */
250
+ async getAccountInfoExtended(accountId, token) {
251
+ return this.request(`/api/accounts/${accountId}`, {
252
+ method: "GET",
253
+ token
254
+ });
255
+ }
256
+ /**
257
+ * Add a recovery passkey to an existing account
258
+ *
259
+ * @param accountId - Account ID
260
+ * @param request - Passkey data (COSE public key, credential ID, algorithm, label)
261
+ * @param token - JWT token
262
+ * @returns Success response with label
263
+ */
264
+ async addPasskey(accountId, request, token) {
265
+ return this.request(`/api/accounts/${accountId}/passkeys`, {
266
+ method: "POST",
267
+ token,
268
+ body: JSON.stringify(request)
269
+ });
270
+ }
271
+ /**
272
+ * Remove a passkey from an account
273
+ *
274
+ * Note: Cannot remove the owner passkey (index 0)
275
+ *
276
+ * @param accountId - Account ID
277
+ * @param passkeyIndex - Index of passkey to remove (1-3 for recovery passkeys)
278
+ * @param token - JWT token
279
+ * @returns Success response
280
+ */
281
+ async removePasskey(accountId, passkeyIndex, token) {
282
+ return this.request(`/api/accounts/${accountId}/passkeys/${passkeyIndex}`, {
283
+ method: "DELETE",
284
+ token
285
+ });
286
+ }
218
287
  /**
219
288
  * Health check
220
289
  *
@@ -241,6 +310,281 @@ var KentuckySignerClient = class {
241
310
  });
242
311
  return response.version;
243
312
  }
313
+ // ============================================================================
314
+ // Guardian Management
315
+ // ============================================================================
316
+ /**
317
+ * Add a guardian passkey to an account
318
+ *
319
+ * Guardians can participate in account recovery but cannot access the wallet.
320
+ * An account can have up to 3 guardians (indices 1-3).
321
+ *
322
+ * @param request - Guardian data (attestation object and optional label)
323
+ * @param token - JWT token
324
+ * @returns Success response with guardian index and count
325
+ */
326
+ async addGuardian(request, token) {
327
+ return this.request("/api/guardians/add", {
328
+ method: "POST",
329
+ token,
330
+ body: JSON.stringify(request)
331
+ });
332
+ }
333
+ /**
334
+ * Remove a guardian passkey from an account
335
+ *
336
+ * Cannot remove guardians during an active recovery.
337
+ *
338
+ * @param guardianIndex - Index of guardian to remove (1-3)
339
+ * @param token - JWT token
340
+ * @returns Success response with remaining guardian count
341
+ */
342
+ async removeGuardian(guardianIndex, token) {
343
+ return this.request("/api/guardians/remove", {
344
+ method: "POST",
345
+ token,
346
+ body: JSON.stringify({ guardian_index: guardianIndex })
347
+ });
348
+ }
349
+ /**
350
+ * Get guardians for an account
351
+ *
352
+ * @param token - JWT token
353
+ * @returns Guardian list with indices and labels
354
+ */
355
+ async getGuardians(token) {
356
+ return this.request("/api/guardians", {
357
+ method: "GET",
358
+ token
359
+ });
360
+ }
361
+ // ============================================================================
362
+ // Account Recovery
363
+ // ============================================================================
364
+ /**
365
+ * Initiate account recovery
366
+ *
367
+ * Call this when you've lost access to your account. You'll need to register
368
+ * a new passkey which will become the new owner passkey after recovery completes.
369
+ *
370
+ * @param accountId - Account ID to recover
371
+ * @param attestationObject - WebAuthn attestation object for new owner passkey
372
+ * @param label - Optional label for new owner passkey
373
+ * @returns Challenges for guardians to sign, threshold, and timelock info
374
+ */
375
+ async initiateRecovery(accountId, attestationObject, label) {
376
+ const body = {
377
+ account_id: accountId,
378
+ attestation_object: attestationObject
379
+ };
380
+ if (label) {
381
+ body.label = label;
382
+ }
383
+ return this.request("/api/recovery/initiate", {
384
+ method: "POST",
385
+ body: JSON.stringify(body)
386
+ });
387
+ }
388
+ /**
389
+ * Submit a guardian signature for recovery
390
+ *
391
+ * Each guardian must sign their challenge using their passkey.
392
+ *
393
+ * @param request - Guardian signature data
394
+ * @returns Current verification status
395
+ */
396
+ async verifyGuardian(request) {
397
+ return this.request("/api/recovery/verify", {
398
+ method: "POST",
399
+ body: JSON.stringify(request)
400
+ });
401
+ }
402
+ /**
403
+ * Get recovery status for an account
404
+ *
405
+ * @param accountId - Account ID to check
406
+ * @returns Recovery status including verification count and timelock
407
+ */
408
+ async getRecoveryStatus(accountId) {
409
+ return this.request("/api/recovery/status", {
410
+ method: "POST",
411
+ body: JSON.stringify({ account_id: accountId })
412
+ });
413
+ }
414
+ /**
415
+ * Complete account recovery
416
+ *
417
+ * Call this after enough guardians have verified and the timelock has expired.
418
+ * The new owner passkey will replace the old one.
419
+ *
420
+ * @param accountId - Account ID to complete recovery for
421
+ * @returns Success message
422
+ */
423
+ async completeRecovery(accountId) {
424
+ return this.request("/api/recovery/complete", {
425
+ method: "POST",
426
+ body: JSON.stringify({ account_id: accountId })
427
+ });
428
+ }
429
+ /**
430
+ * Cancel a pending recovery
431
+ *
432
+ * Only the current owner (with valid auth) can cancel a recovery.
433
+ * Use this if you regain access and want to stop an unauthorized recovery.
434
+ *
435
+ * @param token - JWT token (must be authenticated as owner)
436
+ * @returns Success message
437
+ */
438
+ async cancelRecovery(token) {
439
+ return this.request("/api/recovery/cancel", {
440
+ method: "POST",
441
+ token
442
+ });
443
+ }
444
+ // ============================================================================
445
+ // Two-Factor Authentication (2FA)
446
+ // ============================================================================
447
+ /**
448
+ * Get 2FA status for the authenticated account
449
+ *
450
+ * @param token - JWT token
451
+ * @returns 2FA status including TOTP and PIN enablement
452
+ */
453
+ async get2FAStatus(token) {
454
+ return this.request("/api/2fa/status", {
455
+ method: "GET",
456
+ token
457
+ });
458
+ }
459
+ /**
460
+ * Setup TOTP for the account
461
+ *
462
+ * Returns an otpauth:// URI for QR code generation and a base32 secret
463
+ * for manual entry. After setup, call enableTOTP with a valid code to
464
+ * complete the setup.
465
+ *
466
+ * @param token - JWT token
467
+ * @returns TOTP setup response with URI and secret
468
+ */
469
+ async setupTOTP(token) {
470
+ return this.request("/api/2fa/totp/setup", {
471
+ method: "POST",
472
+ token
473
+ });
474
+ }
475
+ /**
476
+ * Verify and enable TOTP for the account
477
+ *
478
+ * Call this after setupTOTP with a valid code from the authenticator app.
479
+ *
480
+ * @param code - 6-digit TOTP code from authenticator app
481
+ * @param token - JWT token
482
+ * @returns Success response
483
+ */
484
+ async enableTOTP(code, token) {
485
+ return this.request("/api/2fa/totp/enable", {
486
+ method: "POST",
487
+ token,
488
+ body: JSON.stringify({ code })
489
+ });
490
+ }
491
+ /**
492
+ * Disable TOTP for the account
493
+ *
494
+ * Requires a valid TOTP code for confirmation.
495
+ *
496
+ * @param code - Current 6-digit TOTP code
497
+ * @param token - JWT token
498
+ * @returns Success response
499
+ */
500
+ async disableTOTP(code, token) {
501
+ return this.request("/api/2fa/totp/disable", {
502
+ method: "POST",
503
+ token,
504
+ body: JSON.stringify({ code })
505
+ });
506
+ }
507
+ /**
508
+ * Verify a TOTP code
509
+ *
510
+ * Use this to verify a TOTP code without performing any other operation.
511
+ *
512
+ * @param code - 6-digit TOTP code
513
+ * @param token - JWT token
514
+ * @returns Verification result
515
+ */
516
+ async verifyTOTP(code, token) {
517
+ return this.request("/api/2fa/totp/verify", {
518
+ method: "POST",
519
+ token,
520
+ body: JSON.stringify({ code })
521
+ });
522
+ }
523
+ /**
524
+ * Setup PIN for the account
525
+ *
526
+ * Sets a 4-digit or 6-digit PIN. If a PIN is already set, this replaces it.
527
+ *
528
+ * @param pin - 4 or 6 digit PIN
529
+ * @param token - JWT token
530
+ * @returns Success response with PIN length
531
+ */
532
+ async setupPIN(pin, token) {
533
+ return this.request("/api/2fa/pin/setup", {
534
+ method: "POST",
535
+ token,
536
+ body: JSON.stringify({ pin })
537
+ });
538
+ }
539
+ /**
540
+ * Disable PIN for the account
541
+ *
542
+ * Requires the current PIN for confirmation.
543
+ *
544
+ * @param pin - Current PIN
545
+ * @param token - JWT token
546
+ * @returns Success response
547
+ */
548
+ async disablePIN(pin, token) {
549
+ return this.request("/api/2fa/pin/disable", {
550
+ method: "POST",
551
+ token,
552
+ body: JSON.stringify({ pin })
553
+ });
554
+ }
555
+ /**
556
+ * Verify a PIN
557
+ *
558
+ * Use this to verify a PIN without performing any other operation.
559
+ *
560
+ * @param pin - PIN to verify
561
+ * @param token - JWT token
562
+ * @returns Verification result
563
+ */
564
+ async verifyPIN(pin, token) {
565
+ return this.request("/api/2fa/pin/verify", {
566
+ method: "POST",
567
+ token,
568
+ body: JSON.stringify({ pin })
569
+ });
570
+ }
571
+ /**
572
+ * Sign an EVM transaction with 2FA
573
+ *
574
+ * Use this method when 2FA is enabled. If 2FA is not enabled,
575
+ * you can use the regular signEvmTransaction method instead.
576
+ *
577
+ * @param request - Sign request including tx_hash, chain_id, and optional 2FA codes
578
+ * @param token - JWT token
579
+ * @returns Signature response with r, s, v components
580
+ */
581
+ async signEvmTransactionWith2FA(request, token) {
582
+ return this.request("/api/sign/evm", {
583
+ method: "POST",
584
+ token,
585
+ body: JSON.stringify(request)
586
+ });
587
+ }
244
588
  };
245
589
  function createClient(options) {
246
590
  return new KentuckySignerClient(options);
@@ -248,9 +592,9 @@ function createClient(options) {
248
592
 
249
593
  // src/account.ts
250
594
  function createKentuckySignerAccount(options) {
251
- const { config, defaultChainId = 1, onSessionExpired } = options;
595
+ const { config, defaultChainId = 1, onSessionExpired, secureClient, on2FARequired } = options;
252
596
  let session = options.session;
253
- const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
597
+ const client = secureClient ?? new KentuckySignerClient({ baseUrl: config.baseUrl });
254
598
  async function getToken() {
255
599
  if (Date.now() + 6e4 >= session.expiresAt) {
256
600
  if (onSessionExpired) {
@@ -267,17 +611,63 @@ function createKentuckySignerAccount(options) {
267
611
  }
268
612
  async function signHash(hash, chainId) {
269
613
  const token = await getToken();
270
- const response = await client.signEvmTransaction(
271
- { tx_hash: hash, chain_id: chainId },
272
- token
273
- );
274
- return response.signature.full;
614
+ try {
615
+ const response = await client.signEvmTransaction(
616
+ { tx_hash: hash, chain_id: chainId },
617
+ token
618
+ );
619
+ return response.signature.full;
620
+ } catch (err) {
621
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
622
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
623
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
624
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
625
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
626
+ if (!codes) {
627
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
628
+ }
629
+ const response = await client.signEvmTransactionWith2FA(
630
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
631
+ token
632
+ );
633
+ return response.signature.full;
634
+ }
635
+ throw err;
636
+ }
275
637
  }
276
- function parseSignature(signature) {
277
- const r = `0x${signature.slice(2, 66)}`;
278
- const s = `0x${signature.slice(66, 130)}`;
279
- const v = BigInt(`0x${signature.slice(130, 132)}`);
280
- return { r, s, v };
638
+ async function signHashWithComponents(hash, chainId) {
639
+ const token = await getToken();
640
+ try {
641
+ const response = await client.signEvmTransaction(
642
+ { tx_hash: hash, chain_id: chainId },
643
+ token
644
+ );
645
+ return {
646
+ r: response.signature.r,
647
+ s: response.signature.s,
648
+ v: response.signature.v
649
+ };
650
+ } catch (err) {
651
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
652
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
653
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
654
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
655
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
656
+ if (!codes) {
657
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
658
+ }
659
+ const response = await client.signEvmTransactionWith2FA(
660
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
661
+ token
662
+ );
663
+ return {
664
+ r: response.signature.r,
665
+ s: response.signature.s,
666
+ v: response.signature.v
667
+ };
668
+ }
669
+ throw err;
670
+ }
281
671
  }
282
672
  const account = toAccount({
283
673
  address: session.evmAddress,
@@ -300,13 +690,12 @@ function createKentuckySignerAccount(options) {
300
690
  const chainId = transaction.chainId ?? defaultChainId;
301
691
  const serializedUnsigned = serializeTransaction(transaction);
302
692
  const txHash = keccak256(serializedUnsigned);
303
- const signature = await signHash(txHash, chainId);
304
- const { r, s, v } = parseSignature(signature);
693
+ const { r, s, v } = await signHashWithComponents(txHash, chainId);
305
694
  let yParity;
306
695
  if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
307
- yParity = Number(v) - 27;
696
+ yParity = v >= 27 ? v - 27 : v;
308
697
  } else {
309
- yParity = Number(v);
698
+ yParity = v;
310
699
  }
311
700
  const serializedSigned = serializeTransaction(transaction, {
312
701
  r,
@@ -443,6 +832,520 @@ function formatError(error) {
443
832
  return "An unknown error occurred";
444
833
  }
445
834
 
835
+ // src/ephemeral.ts
836
+ function isWebCryptoAvailable() {
837
+ return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && typeof crypto.getRandomValues !== "undefined";
838
+ }
839
+ async function generateEphemeralKeyPair() {
840
+ if (!isWebCryptoAvailable()) {
841
+ throw new Error("WebCrypto is not available in this environment");
842
+ }
843
+ const keyPair = await crypto.subtle.generateKey(
844
+ {
845
+ name: "ECDSA",
846
+ namedCurve: "P-256"
847
+ },
848
+ true,
849
+ // extractable (only for public key export)
850
+ ["sign", "verify"]
851
+ );
852
+ const publicKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
853
+ const publicKeyBase64 = base64UrlEncode(new Uint8Array(publicKeyBuffer));
854
+ return {
855
+ publicKey: publicKeyBase64,
856
+ privateKey: keyPair.privateKey,
857
+ algorithm: "ES256",
858
+ createdAt: Date.now()
859
+ };
860
+ }
861
+ async function signPayload(payload, keyPair) {
862
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
863
+ const timestamp = Date.now();
864
+ const message = `${timestamp}.${payloadString}`;
865
+ const messageBytes = new TextEncoder().encode(message);
866
+ const signatureBuffer = await crypto.subtle.sign(
867
+ {
868
+ name: "ECDSA",
869
+ hash: "SHA-256"
870
+ },
871
+ keyPair.privateKey,
872
+ messageBytes
873
+ );
874
+ const signatureBase64 = base64UrlEncode(new Uint8Array(signatureBuffer));
875
+ return {
876
+ payload: payloadString,
877
+ signature: signatureBase64,
878
+ timestamp
879
+ };
880
+ }
881
+ async function verifyPayload(signedPayload, publicKeyBase64) {
882
+ try {
883
+ const publicKeyBytes = base64UrlDecode(publicKeyBase64);
884
+ const publicKey = await crypto.subtle.importKey(
885
+ "spki",
886
+ publicKeyBytes.buffer,
887
+ {
888
+ name: "ECDSA",
889
+ namedCurve: "P-256"
890
+ },
891
+ false,
892
+ ["verify"]
893
+ );
894
+ const message = `${signedPayload.timestamp}.${signedPayload.payload}`;
895
+ const messageBytes = new TextEncoder().encode(message);
896
+ const signatureBytes = base64UrlDecode(signedPayload.signature);
897
+ return await crypto.subtle.verify(
898
+ {
899
+ name: "ECDSA",
900
+ hash: "SHA-256"
901
+ },
902
+ publicKey,
903
+ signatureBytes.buffer,
904
+ messageBytes
905
+ );
906
+ } catch {
907
+ return false;
908
+ }
909
+ }
910
+ var EPHEMERAL_KEY_STORAGE_KEY = "kentucky_signer_ephemeral";
911
+ var MemoryEphemeralKeyStorage = class {
912
+ constructor() {
913
+ this.keyPair = null;
914
+ }
915
+ async save(keyPair) {
916
+ this.keyPair = keyPair;
917
+ }
918
+ async load() {
919
+ return this.keyPair;
920
+ }
921
+ async clear() {
922
+ this.keyPair = null;
923
+ }
924
+ };
925
+ var IndexedDBEphemeralKeyStorage = class {
926
+ constructor() {
927
+ this.dbName = "kentucky_signer_ephemeral_keys";
928
+ this.storeName = "keys";
929
+ }
930
+ async getDB() {
931
+ return new Promise((resolve, reject) => {
932
+ const request = indexedDB.open(this.dbName, 1);
933
+ request.onerror = () => reject(request.error);
934
+ request.onsuccess = () => resolve(request.result);
935
+ request.onupgradeneeded = () => {
936
+ const db = request.result;
937
+ if (!db.objectStoreNames.contains(this.storeName)) {
938
+ db.createObjectStore(this.storeName);
939
+ }
940
+ };
941
+ });
942
+ }
943
+ async save(keyPair) {
944
+ const db = await this.getDB();
945
+ return new Promise((resolve, reject) => {
946
+ const tx = db.transaction(this.storeName, "readwrite");
947
+ const store = tx.objectStore(this.storeName);
948
+ const data = {
949
+ publicKey: keyPair.publicKey,
950
+ privateKey: keyPair.privateKey,
951
+ algorithm: keyPair.algorithm,
952
+ createdAt: keyPair.createdAt
953
+ };
954
+ const request = store.put(data, EPHEMERAL_KEY_STORAGE_KEY);
955
+ request.onerror = () => reject(request.error);
956
+ request.onsuccess = () => resolve();
957
+ });
958
+ }
959
+ async load() {
960
+ const db = await this.getDB();
961
+ return new Promise((resolve, reject) => {
962
+ const tx = db.transaction(this.storeName, "readonly");
963
+ const store = tx.objectStore(this.storeName);
964
+ const request = store.get(EPHEMERAL_KEY_STORAGE_KEY);
965
+ request.onerror = () => reject(request.error);
966
+ request.onsuccess = () => {
967
+ if (request.result) {
968
+ resolve({
969
+ publicKey: request.result.publicKey,
970
+ privateKey: request.result.privateKey,
971
+ algorithm: request.result.algorithm,
972
+ createdAt: request.result.createdAt
973
+ });
974
+ } else {
975
+ resolve(null);
976
+ }
977
+ };
978
+ });
979
+ }
980
+ async clear() {
981
+ const db = await this.getDB();
982
+ return new Promise((resolve, reject) => {
983
+ const tx = db.transaction(this.storeName, "readwrite");
984
+ const store = tx.objectStore(this.storeName);
985
+ const request = store.delete(EPHEMERAL_KEY_STORAGE_KEY);
986
+ request.onerror = () => reject(request.error);
987
+ request.onsuccess = () => resolve();
988
+ });
989
+ }
990
+ };
991
+ var EphemeralKeyManager = class {
992
+ constructor(storage) {
993
+ this.keyPair = null;
994
+ this.storage = storage ?? new MemoryEphemeralKeyStorage();
995
+ }
996
+ /**
997
+ * Get or generate ephemeral key pair
998
+ */
999
+ async getKeyPair() {
1000
+ if (!this.keyPair) {
1001
+ this.keyPair = await this.storage.load();
1002
+ }
1003
+ if (!this.keyPair) {
1004
+ this.keyPair = await generateEphemeralKeyPair();
1005
+ await this.storage.save(this.keyPair);
1006
+ }
1007
+ return this.keyPair;
1008
+ }
1009
+ /**
1010
+ * Get the public key for authentication
1011
+ */
1012
+ async getPublicKey() {
1013
+ const keyPair = await this.getKeyPair();
1014
+ return keyPair.publicKey;
1015
+ }
1016
+ /**
1017
+ * Sign a request payload
1018
+ */
1019
+ async signPayload(payload) {
1020
+ const keyPair = await this.getKeyPair();
1021
+ return signPayload(payload, keyPair);
1022
+ }
1023
+ /**
1024
+ * Rotate the key pair (generate new keys)
1025
+ */
1026
+ async rotate() {
1027
+ await this.storage.clear();
1028
+ this.keyPair = await generateEphemeralKeyPair();
1029
+ await this.storage.save(this.keyPair);
1030
+ return this.keyPair;
1031
+ }
1032
+ /**
1033
+ * Clear the key pair (logout)
1034
+ */
1035
+ async clear() {
1036
+ await this.storage.clear();
1037
+ this.keyPair = null;
1038
+ }
1039
+ /**
1040
+ * Check if a key pair exists
1041
+ */
1042
+ async hasKeyPair() {
1043
+ if (this.keyPair) return true;
1044
+ const loaded = await this.storage.load();
1045
+ return loaded !== null;
1046
+ }
1047
+ /**
1048
+ * Migrate key pair to a new storage backend
1049
+ *
1050
+ * This preserves the existing key pair while switching storage.
1051
+ * The key is saved to the new storage and removed from the old storage.
1052
+ *
1053
+ * @param newStorage - The new storage backend to migrate to
1054
+ */
1055
+ async migrateStorage(newStorage) {
1056
+ const currentKeyPair = this.keyPair ?? await this.storage.load();
1057
+ await this.storage.clear();
1058
+ this.storage = newStorage;
1059
+ if (currentKeyPair) {
1060
+ await this.storage.save(currentKeyPair);
1061
+ this.keyPair = currentKeyPair;
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Get the current storage backend
1066
+ */
1067
+ getStorage() {
1068
+ return this.storage;
1069
+ }
1070
+ };
1071
+
1072
+ // src/secure-client.ts
1073
+ var SecureKentuckySignerClient = class {
1074
+ constructor(options) {
1075
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
1076
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
1077
+ this.timeout = options.timeout ?? 3e4;
1078
+ this.keyManager = options.ephemeralKeyManager ?? new EphemeralKeyManager(
1079
+ options.ephemeralKeyStorage ?? new MemoryEphemeralKeyStorage()
1080
+ );
1081
+ this.requireSigning = options.requireEphemeralSigning ?? true;
1082
+ }
1083
+ /**
1084
+ * Get the ephemeral key manager for advanced usage
1085
+ */
1086
+ getKeyManager() {
1087
+ return this.keyManager;
1088
+ }
1089
+ /**
1090
+ * Make a signed request to the API
1091
+ */
1092
+ async request(path, options = {}) {
1093
+ const { token, skipSigning, ...fetchOptions } = options;
1094
+ const headers = {
1095
+ "Content-Type": "application/json",
1096
+ ...options.headers
1097
+ };
1098
+ if (token) {
1099
+ ;
1100
+ headers["Authorization"] = `Bearer ${token}`;
1101
+ }
1102
+ let body = options.body;
1103
+ if (token && !skipSigning && this.requireSigning && body) {
1104
+ const signedPayload = await this.keyManager.signPayload(body);
1105
+ headers["X-Ephemeral-Signature"] = signedPayload.signature;
1106
+ headers["X-Ephemeral-Timestamp"] = signedPayload.timestamp.toString();
1107
+ }
1108
+ const controller = new AbortController();
1109
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1110
+ try {
1111
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
1112
+ ...fetchOptions,
1113
+ headers,
1114
+ body,
1115
+ signal: controller.signal
1116
+ });
1117
+ const data = await response.json();
1118
+ if (!response.ok || data.success === false) {
1119
+ const error = data;
1120
+ throw new KentuckySignerError(
1121
+ error.error?.message ?? "Unknown error",
1122
+ error.error?.code ?? "UNKNOWN_ERROR",
1123
+ error.error?.details
1124
+ );
1125
+ }
1126
+ return data;
1127
+ } finally {
1128
+ clearTimeout(timeoutId);
1129
+ }
1130
+ }
1131
+ /**
1132
+ * Get a challenge for passkey authentication
1133
+ */
1134
+ async getChallenge(accountId) {
1135
+ return this.request("/api/auth/challenge", {
1136
+ method: "POST",
1137
+ body: JSON.stringify({ account_id: accountId }),
1138
+ skipSigning: true
1139
+ // No token yet
1140
+ });
1141
+ }
1142
+ /**
1143
+ * Authenticate with a passkey credential
1144
+ *
1145
+ * Automatically sends the ephemeral public key for binding.
1146
+ */
1147
+ async authenticatePasskey(accountId, credential) {
1148
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1149
+ return this.request("/api/auth/passkey", {
1150
+ method: "POST",
1151
+ body: JSON.stringify({
1152
+ account_id: accountId,
1153
+ credential_id: credential.credentialId,
1154
+ client_data_json: credential.clientDataJSON,
1155
+ authenticator_data: credential.authenticatorData,
1156
+ signature: credential.signature,
1157
+ user_handle: credential.userHandle,
1158
+ ephemeral_public_key: ephemeralPublicKey
1159
+ }),
1160
+ skipSigning: true
1161
+ // No token yet
1162
+ });
1163
+ }
1164
+ /**
1165
+ * Authenticate with password
1166
+ *
1167
+ * Automatically sends the ephemeral public key for binding.
1168
+ */
1169
+ async authenticatePassword(request) {
1170
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1171
+ return this.request("/api/auth/password", {
1172
+ method: "POST",
1173
+ body: JSON.stringify({
1174
+ ...request,
1175
+ ephemeral_public_key: ephemeralPublicKey
1176
+ }),
1177
+ skipSigning: true
1178
+ // No token yet
1179
+ });
1180
+ }
1181
+ /**
1182
+ * Refresh an authentication token
1183
+ *
1184
+ * Generates a new ephemeral key pair and binds it to the new token.
1185
+ */
1186
+ async refreshToken(token) {
1187
+ await this.keyManager.rotate();
1188
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1189
+ return this.request("/api/auth/refresh", {
1190
+ method: "POST",
1191
+ token,
1192
+ body: JSON.stringify({
1193
+ ephemeral_public_key: ephemeralPublicKey
1194
+ }),
1195
+ skipSigning: true
1196
+ // Use old token, but send new key
1197
+ });
1198
+ }
1199
+ /**
1200
+ * Logout and invalidate token
1201
+ *
1202
+ * Clears the ephemeral key pair.
1203
+ */
1204
+ async logout(token) {
1205
+ await this.request("/api/auth/logout", {
1206
+ method: "POST",
1207
+ token,
1208
+ skipSigning: true
1209
+ });
1210
+ await this.keyManager.clear();
1211
+ }
1212
+ /**
1213
+ * Get account information (signed request)
1214
+ */
1215
+ async getAccountInfo(accountId, token) {
1216
+ return this.request(`/api/accounts/${accountId}`, {
1217
+ method: "GET",
1218
+ token,
1219
+ skipSigning: true
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Get extended account information (signed request)
1224
+ */
1225
+ async getAccountInfoExtended(accountId, token) {
1226
+ return this.request(
1227
+ `/api/accounts/${accountId}`,
1228
+ {
1229
+ method: "GET",
1230
+ token,
1231
+ skipSigning: true
1232
+ }
1233
+ );
1234
+ }
1235
+ /**
1236
+ * Sign an EVM transaction hash (signed request)
1237
+ */
1238
+ async signEvmTransaction(request, token) {
1239
+ return this.request("/api/sign/evm", {
1240
+ method: "POST",
1241
+ token,
1242
+ body: JSON.stringify(request)
1243
+ });
1244
+ }
1245
+ /**
1246
+ * Sign an EVM transaction hash with 2FA (signed request)
1247
+ */
1248
+ async signEvmTransactionWith2FA(request, token) {
1249
+ return this.request("/api/sign/evm", {
1250
+ method: "POST",
1251
+ token,
1252
+ body: JSON.stringify(request)
1253
+ });
1254
+ }
1255
+ /**
1256
+ * Sign a raw hash for EVM (signed request)
1257
+ */
1258
+ async signHash(hash, chainId, token) {
1259
+ const response = await this.signEvmTransaction(
1260
+ { tx_hash: hash, chain_id: chainId },
1261
+ token
1262
+ );
1263
+ return response.signature.full;
1264
+ }
1265
+ /**
1266
+ * Add password to account (signed request)
1267
+ */
1268
+ async addPassword(accountId, request, token) {
1269
+ return this.request(
1270
+ `/api/accounts/${accountId}/password`,
1271
+ {
1272
+ method: "POST",
1273
+ token,
1274
+ body: JSON.stringify(request)
1275
+ }
1276
+ );
1277
+ }
1278
+ /**
1279
+ * Add passkey to account (signed request)
1280
+ */
1281
+ async addPasskey(accountId, request, token) {
1282
+ return this.request(
1283
+ `/api/accounts/${accountId}/passkeys`,
1284
+ {
1285
+ method: "POST",
1286
+ token,
1287
+ body: JSON.stringify(request)
1288
+ }
1289
+ );
1290
+ }
1291
+ /**
1292
+ * Remove passkey from account (signed request)
1293
+ */
1294
+ async removePasskey(accountId, passkeyIndex, token) {
1295
+ return this.request(
1296
+ `/api/accounts/${accountId}/passkeys/${passkeyIndex}`,
1297
+ {
1298
+ method: "DELETE",
1299
+ token,
1300
+ body: JSON.stringify({ passkey_index: passkeyIndex })
1301
+ }
1302
+ );
1303
+ }
1304
+ /**
1305
+ * Create account with passkey (public endpoint, no signing)
1306
+ */
1307
+ async createAccountWithPasskey(attestationObject, label) {
1308
+ const body = {
1309
+ attestation_object: attestationObject
1310
+ };
1311
+ if (label) {
1312
+ body.label = label;
1313
+ }
1314
+ return this.request("/api/accounts/create/passkey", {
1315
+ method: "POST",
1316
+ body: JSON.stringify(body),
1317
+ skipSigning: true
1318
+ });
1319
+ }
1320
+ /**
1321
+ * Create account with password (public endpoint, no signing)
1322
+ */
1323
+ async createAccountWithPassword(request) {
1324
+ return this.request("/api/accounts/create/password", {
1325
+ method: "POST",
1326
+ body: JSON.stringify(request),
1327
+ skipSigning: true
1328
+ });
1329
+ }
1330
+ /**
1331
+ * Health check (public endpoint)
1332
+ */
1333
+ async healthCheck() {
1334
+ try {
1335
+ const response = await this.request("/api/health", {
1336
+ method: "GET",
1337
+ skipSigning: true
1338
+ });
1339
+ return response.status === "ok";
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+ };
1345
+ function createSecureClient(options) {
1346
+ return new SecureKentuckySignerClient(options);
1347
+ }
1348
+
446
1349
  // src/auth.ts
447
1350
  function isWebAuthnAvailable() {
448
1351
  return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
@@ -537,7 +1440,8 @@ async function authenticateWithPasskey(options) {
537
1440
  const passkeyCredential = credentialToPasskey(credential);
538
1441
  const authResponse = await client.authenticatePasskey(
539
1442
  options.accountId,
540
- passkeyCredential
1443
+ passkeyCredential,
1444
+ options.ephemeralPublicKey
541
1445
  );
542
1446
  const accountInfo = await client.getAccountInfo(
543
1447
  options.accountId,
@@ -625,6 +1529,7 @@ async function registerPasskey(options) {
625
1529
  credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
626
1530
  clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
627
1531
  authenticatorData: base64UrlEncode(new Uint8Array(response.getAuthenticatorData())),
1532
+ attestationObject: base64UrlEncode(new Uint8Array(response.attestationObject)),
628
1533
  signature: "",
629
1534
  // Not applicable for registration
630
1535
  publicKey: base64UrlEncode(new Uint8Array(publicKeyBytes))
@@ -647,10 +1552,14 @@ async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
647
1552
  }
648
1553
  async function authenticateWithPassword(options) {
649
1554
  const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
650
- const authResponse = await client.authenticatePassword({
1555
+ const authRequest = {
651
1556
  account_id: options.accountId,
652
1557
  password: options.password
653
- });
1558
+ };
1559
+ if (options.ephemeralPublicKey) {
1560
+ authRequest.ephemeral_public_key = options.ephemeralPublicKey;
1561
+ }
1562
+ const authResponse = await client.authenticatePassword(authRequest);
654
1563
  const accountInfo = await client.getAccountInfo(
655
1564
  options.accountId,
656
1565
  authResponse.token
@@ -685,10 +1594,14 @@ async function createAccountWithPassword(options) {
685
1594
  });
686
1595
  }
687
1596
  export {
1597
+ EphemeralKeyManager,
1598
+ IndexedDBEphemeralKeyStorage,
688
1599
  KentuckySignerClient,
689
1600
  KentuckySignerError,
690
1601
  LocalStorageTokenStorage,
1602
+ MemoryEphemeralKeyStorage,
691
1603
  MemoryTokenStorage,
1604
+ SecureKentuckySignerClient,
692
1605
  authenticateWithPasskey,
693
1606
  authenticateWithPassword,
694
1607
  authenticateWithToken,
@@ -698,17 +1611,22 @@ export {
698
1611
  createAccountWithPassword,
699
1612
  createClient,
700
1613
  createKentuckySignerAccount,
1614
+ createSecureClient,
701
1615
  createServerAccount,
702
1616
  formatError,
1617
+ generateEphemeralKeyPair,
703
1618
  getJwtExpiration,
704
1619
  hexToBytes,
705
1620
  isSessionValid,
706
1621
  isValidAccountId,
707
1622
  isValidEvmAddress,
708
1623
  isWebAuthnAvailable,
1624
+ isWebCryptoAvailable,
709
1625
  parseJwt,
710
1626
  refreshSessionIfNeeded,
711
1627
  registerPasskey,
1628
+ signPayload,
1629
+ verifyPayload,
712
1630
  withRetry
713
1631
  };
714
1632
  //# sourceMappingURL=index.mjs.map