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.js CHANGED
@@ -20,10 +20,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ EphemeralKeyManager: () => EphemeralKeyManager,
24
+ IndexedDBEphemeralKeyStorage: () => IndexedDBEphemeralKeyStorage,
23
25
  KentuckySignerClient: () => KentuckySignerClient,
24
26
  KentuckySignerError: () => KentuckySignerError,
25
27
  LocalStorageTokenStorage: () => LocalStorageTokenStorage,
28
+ MemoryEphemeralKeyStorage: () => MemoryEphemeralKeyStorage,
26
29
  MemoryTokenStorage: () => MemoryTokenStorage,
30
+ SecureKentuckySignerClient: () => SecureKentuckySignerClient,
27
31
  authenticateWithPasskey: () => authenticateWithPasskey,
28
32
  authenticateWithPassword: () => authenticateWithPassword,
29
33
  authenticateWithToken: () => authenticateWithToken,
@@ -33,17 +37,22 @@ __export(index_exports, {
33
37
  createAccountWithPassword: () => createAccountWithPassword,
34
38
  createClient: () => createClient,
35
39
  createKentuckySignerAccount: () => createKentuckySignerAccount,
40
+ createSecureClient: () => createSecureClient,
36
41
  createServerAccount: () => createServerAccount,
37
42
  formatError: () => formatError,
43
+ generateEphemeralKeyPair: () => generateEphemeralKeyPair,
38
44
  getJwtExpiration: () => getJwtExpiration,
39
45
  hexToBytes: () => hexToBytes,
40
46
  isSessionValid: () => isSessionValid,
41
47
  isValidAccountId: () => isValidAccountId,
42
48
  isValidEvmAddress: () => isValidEvmAddress,
43
49
  isWebAuthnAvailable: () => isWebAuthnAvailable,
50
+ isWebCryptoAvailable: () => isWebCryptoAvailable,
44
51
  parseJwt: () => parseJwt,
45
52
  refreshSessionIfNeeded: () => refreshSessionIfNeeded,
46
53
  registerPasskey: () => registerPasskey,
54
+ signPayload: () => signPayload,
55
+ verifyPayload: () => verifyPayload,
47
56
  withRetry: () => withRetry
48
57
  });
49
58
  module.exports = __toCommonJS(index_exports);
@@ -64,7 +73,7 @@ var KentuckySignerError = class extends Error {
64
73
  var KentuckySignerClient = class {
65
74
  constructor(options) {
66
75
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
67
- this.fetchImpl = options.fetch ?? globalThis.fetch;
76
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
68
77
  this.timeout = options.timeout ?? 3e4;
69
78
  }
70
79
  /**
@@ -119,19 +128,24 @@ var KentuckySignerClient = class {
119
128
  *
120
129
  * @param accountId - Account ID to authenticate
121
130
  * @param credential - WebAuthn credential from navigator.credentials.get()
131
+ * @param ephemeralPublicKey - Optional ephemeral public key for secure mode binding
122
132
  * @returns Authentication response with JWT token
123
133
  */
124
- async authenticatePasskey(accountId, credential) {
134
+ async authenticatePasskey(accountId, credential, ephemeralPublicKey) {
135
+ const body = {
136
+ account_id: accountId,
137
+ credential_id: credential.credentialId,
138
+ client_data_json: credential.clientDataJSON,
139
+ authenticator_data: credential.authenticatorData,
140
+ signature: credential.signature,
141
+ user_handle: credential.userHandle
142
+ };
143
+ if (ephemeralPublicKey) {
144
+ body.ephemeral_public_key = ephemeralPublicKey;
145
+ }
125
146
  return this.request("/api/auth/passkey", {
126
147
  method: "POST",
127
- body: JSON.stringify({
128
- account_id: accountId,
129
- credential_id: credential.credentialId,
130
- client_data_json: credential.clientDataJSON,
131
- authenticator_data: credential.authenticatorData,
132
- signature: credential.signature,
133
- user_handle: credential.userHandle
134
- })
148
+ body: JSON.stringify(body)
135
149
  });
136
150
  }
137
151
  /**
@@ -222,18 +236,20 @@ var KentuckySignerClient = class {
222
236
  /**
223
237
  * Create a new account with passkey authentication
224
238
  *
225
- * @param credential - WebAuthn credential from navigator.credentials.create()
239
+ * @param attestationObject - Base64url encoded attestation object from WebAuthn
240
+ * @param label - Optional label for the passkey (defaults to "Owner Passkey")
226
241
  * @returns Account creation response with account ID and addresses
227
242
  */
228
- async createAccountWithPasskey(credential) {
243
+ async createAccountWithPasskey(attestationObject, label) {
244
+ const body = {
245
+ attestation_object: attestationObject
246
+ };
247
+ if (label) {
248
+ body.label = label;
249
+ }
229
250
  return this.request("/api/accounts/create/passkey", {
230
251
  method: "POST",
231
- body: JSON.stringify({
232
- credential_id: credential.credentialId,
233
- public_key: credential.publicKey,
234
- client_data_json: credential.clientDataJSON,
235
- authenticator_data: credential.authenticatorData
236
- })
252
+ body: JSON.stringify(body)
237
253
  });
238
254
  }
239
255
  /**
@@ -260,6 +276,68 @@ var KentuckySignerClient = class {
260
276
  body: JSON.stringify(request)
261
277
  });
262
278
  }
279
+ /**
280
+ * Add password authentication to an existing account
281
+ *
282
+ * Enables password-based authentication for an account that was created
283
+ * with passkey-only authentication.
284
+ *
285
+ * @param accountId - Account ID
286
+ * @param request - Password and confirmation
287
+ * @param token - JWT token
288
+ * @returns Success response
289
+ */
290
+ async addPassword(accountId, request, token) {
291
+ return this.request(`/api/accounts/${accountId}/password`, {
292
+ method: "POST",
293
+ token,
294
+ body: JSON.stringify(request)
295
+ });
296
+ }
297
+ /**
298
+ * Get extended account information including auth config
299
+ *
300
+ * @param accountId - Account ID
301
+ * @param token - JWT token
302
+ * @returns Extended account info with auth config and passkey count
303
+ */
304
+ async getAccountInfoExtended(accountId, token) {
305
+ return this.request(`/api/accounts/${accountId}`, {
306
+ method: "GET",
307
+ token
308
+ });
309
+ }
310
+ /**
311
+ * Add a recovery passkey to an existing account
312
+ *
313
+ * @param accountId - Account ID
314
+ * @param request - Passkey data (COSE public key, credential ID, algorithm, label)
315
+ * @param token - JWT token
316
+ * @returns Success response with label
317
+ */
318
+ async addPasskey(accountId, request, token) {
319
+ return this.request(`/api/accounts/${accountId}/passkeys`, {
320
+ method: "POST",
321
+ token,
322
+ body: JSON.stringify(request)
323
+ });
324
+ }
325
+ /**
326
+ * Remove a passkey from an account
327
+ *
328
+ * Note: Cannot remove the owner passkey (index 0)
329
+ *
330
+ * @param accountId - Account ID
331
+ * @param passkeyIndex - Index of passkey to remove (1-3 for recovery passkeys)
332
+ * @param token - JWT token
333
+ * @returns Success response
334
+ */
335
+ async removePasskey(accountId, passkeyIndex, token) {
336
+ return this.request(`/api/accounts/${accountId}/passkeys/${passkeyIndex}`, {
337
+ method: "DELETE",
338
+ token
339
+ });
340
+ }
263
341
  /**
264
342
  * Health check
265
343
  *
@@ -286,6 +364,281 @@ var KentuckySignerClient = class {
286
364
  });
287
365
  return response.version;
288
366
  }
367
+ // ============================================================================
368
+ // Guardian Management
369
+ // ============================================================================
370
+ /**
371
+ * Add a guardian passkey to an account
372
+ *
373
+ * Guardians can participate in account recovery but cannot access the wallet.
374
+ * An account can have up to 3 guardians (indices 1-3).
375
+ *
376
+ * @param request - Guardian data (attestation object and optional label)
377
+ * @param token - JWT token
378
+ * @returns Success response with guardian index and count
379
+ */
380
+ async addGuardian(request, token) {
381
+ return this.request("/api/guardians/add", {
382
+ method: "POST",
383
+ token,
384
+ body: JSON.stringify(request)
385
+ });
386
+ }
387
+ /**
388
+ * Remove a guardian passkey from an account
389
+ *
390
+ * Cannot remove guardians during an active recovery.
391
+ *
392
+ * @param guardianIndex - Index of guardian to remove (1-3)
393
+ * @param token - JWT token
394
+ * @returns Success response with remaining guardian count
395
+ */
396
+ async removeGuardian(guardianIndex, token) {
397
+ return this.request("/api/guardians/remove", {
398
+ method: "POST",
399
+ token,
400
+ body: JSON.stringify({ guardian_index: guardianIndex })
401
+ });
402
+ }
403
+ /**
404
+ * Get guardians for an account
405
+ *
406
+ * @param token - JWT token
407
+ * @returns Guardian list with indices and labels
408
+ */
409
+ async getGuardians(token) {
410
+ return this.request("/api/guardians", {
411
+ method: "GET",
412
+ token
413
+ });
414
+ }
415
+ // ============================================================================
416
+ // Account Recovery
417
+ // ============================================================================
418
+ /**
419
+ * Initiate account recovery
420
+ *
421
+ * Call this when you've lost access to your account. You'll need to register
422
+ * a new passkey which will become the new owner passkey after recovery completes.
423
+ *
424
+ * @param accountId - Account ID to recover
425
+ * @param attestationObject - WebAuthn attestation object for new owner passkey
426
+ * @param label - Optional label for new owner passkey
427
+ * @returns Challenges for guardians to sign, threshold, and timelock info
428
+ */
429
+ async initiateRecovery(accountId, attestationObject, label) {
430
+ const body = {
431
+ account_id: accountId,
432
+ attestation_object: attestationObject
433
+ };
434
+ if (label) {
435
+ body.label = label;
436
+ }
437
+ return this.request("/api/recovery/initiate", {
438
+ method: "POST",
439
+ body: JSON.stringify(body)
440
+ });
441
+ }
442
+ /**
443
+ * Submit a guardian signature for recovery
444
+ *
445
+ * Each guardian must sign their challenge using their passkey.
446
+ *
447
+ * @param request - Guardian signature data
448
+ * @returns Current verification status
449
+ */
450
+ async verifyGuardian(request) {
451
+ return this.request("/api/recovery/verify", {
452
+ method: "POST",
453
+ body: JSON.stringify(request)
454
+ });
455
+ }
456
+ /**
457
+ * Get recovery status for an account
458
+ *
459
+ * @param accountId - Account ID to check
460
+ * @returns Recovery status including verification count and timelock
461
+ */
462
+ async getRecoveryStatus(accountId) {
463
+ return this.request("/api/recovery/status", {
464
+ method: "POST",
465
+ body: JSON.stringify({ account_id: accountId })
466
+ });
467
+ }
468
+ /**
469
+ * Complete account recovery
470
+ *
471
+ * Call this after enough guardians have verified and the timelock has expired.
472
+ * The new owner passkey will replace the old one.
473
+ *
474
+ * @param accountId - Account ID to complete recovery for
475
+ * @returns Success message
476
+ */
477
+ async completeRecovery(accountId) {
478
+ return this.request("/api/recovery/complete", {
479
+ method: "POST",
480
+ body: JSON.stringify({ account_id: accountId })
481
+ });
482
+ }
483
+ /**
484
+ * Cancel a pending recovery
485
+ *
486
+ * Only the current owner (with valid auth) can cancel a recovery.
487
+ * Use this if you regain access and want to stop an unauthorized recovery.
488
+ *
489
+ * @param token - JWT token (must be authenticated as owner)
490
+ * @returns Success message
491
+ */
492
+ async cancelRecovery(token) {
493
+ return this.request("/api/recovery/cancel", {
494
+ method: "POST",
495
+ token
496
+ });
497
+ }
498
+ // ============================================================================
499
+ // Two-Factor Authentication (2FA)
500
+ // ============================================================================
501
+ /**
502
+ * Get 2FA status for the authenticated account
503
+ *
504
+ * @param token - JWT token
505
+ * @returns 2FA status including TOTP and PIN enablement
506
+ */
507
+ async get2FAStatus(token) {
508
+ return this.request("/api/2fa/status", {
509
+ method: "GET",
510
+ token
511
+ });
512
+ }
513
+ /**
514
+ * Setup TOTP for the account
515
+ *
516
+ * Returns an otpauth:// URI for QR code generation and a base32 secret
517
+ * for manual entry. After setup, call enableTOTP with a valid code to
518
+ * complete the setup.
519
+ *
520
+ * @param token - JWT token
521
+ * @returns TOTP setup response with URI and secret
522
+ */
523
+ async setupTOTP(token) {
524
+ return this.request("/api/2fa/totp/setup", {
525
+ method: "POST",
526
+ token
527
+ });
528
+ }
529
+ /**
530
+ * Verify and enable TOTP for the account
531
+ *
532
+ * Call this after setupTOTP with a valid code from the authenticator app.
533
+ *
534
+ * @param code - 6-digit TOTP code from authenticator app
535
+ * @param token - JWT token
536
+ * @returns Success response
537
+ */
538
+ async enableTOTP(code, token) {
539
+ return this.request("/api/2fa/totp/enable", {
540
+ method: "POST",
541
+ token,
542
+ body: JSON.stringify({ code })
543
+ });
544
+ }
545
+ /**
546
+ * Disable TOTP for the account
547
+ *
548
+ * Requires a valid TOTP code for confirmation.
549
+ *
550
+ * @param code - Current 6-digit TOTP code
551
+ * @param token - JWT token
552
+ * @returns Success response
553
+ */
554
+ async disableTOTP(code, token) {
555
+ return this.request("/api/2fa/totp/disable", {
556
+ method: "POST",
557
+ token,
558
+ body: JSON.stringify({ code })
559
+ });
560
+ }
561
+ /**
562
+ * Verify a TOTP code
563
+ *
564
+ * Use this to verify a TOTP code without performing any other operation.
565
+ *
566
+ * @param code - 6-digit TOTP code
567
+ * @param token - JWT token
568
+ * @returns Verification result
569
+ */
570
+ async verifyTOTP(code, token) {
571
+ return this.request("/api/2fa/totp/verify", {
572
+ method: "POST",
573
+ token,
574
+ body: JSON.stringify({ code })
575
+ });
576
+ }
577
+ /**
578
+ * Setup PIN for the account
579
+ *
580
+ * Sets a 4-digit or 6-digit PIN. If a PIN is already set, this replaces it.
581
+ *
582
+ * @param pin - 4 or 6 digit PIN
583
+ * @param token - JWT token
584
+ * @returns Success response with PIN length
585
+ */
586
+ async setupPIN(pin, token) {
587
+ return this.request("/api/2fa/pin/setup", {
588
+ method: "POST",
589
+ token,
590
+ body: JSON.stringify({ pin })
591
+ });
592
+ }
593
+ /**
594
+ * Disable PIN for the account
595
+ *
596
+ * Requires the current PIN for confirmation.
597
+ *
598
+ * @param pin - Current PIN
599
+ * @param token - JWT token
600
+ * @returns Success response
601
+ */
602
+ async disablePIN(pin, token) {
603
+ return this.request("/api/2fa/pin/disable", {
604
+ method: "POST",
605
+ token,
606
+ body: JSON.stringify({ pin })
607
+ });
608
+ }
609
+ /**
610
+ * Verify a PIN
611
+ *
612
+ * Use this to verify a PIN without performing any other operation.
613
+ *
614
+ * @param pin - PIN to verify
615
+ * @param token - JWT token
616
+ * @returns Verification result
617
+ */
618
+ async verifyPIN(pin, token) {
619
+ return this.request("/api/2fa/pin/verify", {
620
+ method: "POST",
621
+ token,
622
+ body: JSON.stringify({ pin })
623
+ });
624
+ }
625
+ /**
626
+ * Sign an EVM transaction with 2FA
627
+ *
628
+ * Use this method when 2FA is enabled. If 2FA is not enabled,
629
+ * you can use the regular signEvmTransaction method instead.
630
+ *
631
+ * @param request - Sign request including tx_hash, chain_id, and optional 2FA codes
632
+ * @param token - JWT token
633
+ * @returns Signature response with r, s, v components
634
+ */
635
+ async signEvmTransactionWith2FA(request, token) {
636
+ return this.request("/api/sign/evm", {
637
+ method: "POST",
638
+ token,
639
+ body: JSON.stringify(request)
640
+ });
641
+ }
289
642
  };
290
643
  function createClient(options) {
291
644
  return new KentuckySignerClient(options);
@@ -293,9 +646,9 @@ function createClient(options) {
293
646
 
294
647
  // src/account.ts
295
648
  function createKentuckySignerAccount(options) {
296
- const { config, defaultChainId = 1, onSessionExpired } = options;
649
+ const { config, defaultChainId = 1, onSessionExpired, secureClient, on2FARequired } = options;
297
650
  let session = options.session;
298
- const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
651
+ const client = secureClient ?? new KentuckySignerClient({ baseUrl: config.baseUrl });
299
652
  async function getToken() {
300
653
  if (Date.now() + 6e4 >= session.expiresAt) {
301
654
  if (onSessionExpired) {
@@ -312,17 +665,63 @@ function createKentuckySignerAccount(options) {
312
665
  }
313
666
  async function signHash(hash, chainId) {
314
667
  const token = await getToken();
315
- const response = await client.signEvmTransaction(
316
- { tx_hash: hash, chain_id: chainId },
317
- token
318
- );
319
- return response.signature.full;
668
+ try {
669
+ const response = await client.signEvmTransaction(
670
+ { tx_hash: hash, chain_id: chainId },
671
+ token
672
+ );
673
+ return response.signature.full;
674
+ } catch (err) {
675
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
676
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
677
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
678
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
679
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
680
+ if (!codes) {
681
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
682
+ }
683
+ const response = await client.signEvmTransactionWith2FA(
684
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
685
+ token
686
+ );
687
+ return response.signature.full;
688
+ }
689
+ throw err;
690
+ }
320
691
  }
321
- function parseSignature(signature) {
322
- const r = `0x${signature.slice(2, 66)}`;
323
- const s = `0x${signature.slice(66, 130)}`;
324
- const v = BigInt(`0x${signature.slice(130, 132)}`);
325
- return { r, s, v };
692
+ async function signHashWithComponents(hash, chainId) {
693
+ const token = await getToken();
694
+ try {
695
+ const response = await client.signEvmTransaction(
696
+ { tx_hash: hash, chain_id: chainId },
697
+ token
698
+ );
699
+ return {
700
+ r: response.signature.r,
701
+ s: response.signature.s,
702
+ v: response.signature.v
703
+ };
704
+ } catch (err) {
705
+ if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
706
+ const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
707
+ const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
708
+ const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
709
+ const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
710
+ if (!codes) {
711
+ throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
712
+ }
713
+ const response = await client.signEvmTransactionWith2FA(
714
+ { tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
715
+ token
716
+ );
717
+ return {
718
+ r: response.signature.r,
719
+ s: response.signature.s,
720
+ v: response.signature.v
721
+ };
722
+ }
723
+ throw err;
724
+ }
326
725
  }
327
726
  const account = (0, import_accounts.toAccount)({
328
727
  address: session.evmAddress,
@@ -345,13 +744,12 @@ function createKentuckySignerAccount(options) {
345
744
  const chainId = transaction.chainId ?? defaultChainId;
346
745
  const serializedUnsigned = (0, import_viem.serializeTransaction)(transaction);
347
746
  const txHash = (0, import_viem.keccak256)(serializedUnsigned);
348
- const signature = await signHash(txHash, chainId);
349
- const { r, s, v } = parseSignature(signature);
747
+ const { r, s, v } = await signHashWithComponents(txHash, chainId);
350
748
  let yParity;
351
749
  if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
352
- yParity = Number(v) - 27;
750
+ yParity = v >= 27 ? v - 27 : v;
353
751
  } else {
354
- yParity = Number(v);
752
+ yParity = v;
355
753
  }
356
754
  const serializedSigned = (0, import_viem.serializeTransaction)(transaction, {
357
755
  r,
@@ -488,6 +886,520 @@ function formatError(error) {
488
886
  return "An unknown error occurred";
489
887
  }
490
888
 
889
+ // src/ephemeral.ts
890
+ function isWebCryptoAvailable() {
891
+ return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && typeof crypto.getRandomValues !== "undefined";
892
+ }
893
+ async function generateEphemeralKeyPair() {
894
+ if (!isWebCryptoAvailable()) {
895
+ throw new Error("WebCrypto is not available in this environment");
896
+ }
897
+ const keyPair = await crypto.subtle.generateKey(
898
+ {
899
+ name: "ECDSA",
900
+ namedCurve: "P-256"
901
+ },
902
+ true,
903
+ // extractable (only for public key export)
904
+ ["sign", "verify"]
905
+ );
906
+ const publicKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
907
+ const publicKeyBase64 = base64UrlEncode(new Uint8Array(publicKeyBuffer));
908
+ return {
909
+ publicKey: publicKeyBase64,
910
+ privateKey: keyPair.privateKey,
911
+ algorithm: "ES256",
912
+ createdAt: Date.now()
913
+ };
914
+ }
915
+ async function signPayload(payload, keyPair) {
916
+ const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
917
+ const timestamp = Date.now();
918
+ const message = `${timestamp}.${payloadString}`;
919
+ const messageBytes = new TextEncoder().encode(message);
920
+ const signatureBuffer = await crypto.subtle.sign(
921
+ {
922
+ name: "ECDSA",
923
+ hash: "SHA-256"
924
+ },
925
+ keyPair.privateKey,
926
+ messageBytes
927
+ );
928
+ const signatureBase64 = base64UrlEncode(new Uint8Array(signatureBuffer));
929
+ return {
930
+ payload: payloadString,
931
+ signature: signatureBase64,
932
+ timestamp
933
+ };
934
+ }
935
+ async function verifyPayload(signedPayload, publicKeyBase64) {
936
+ try {
937
+ const publicKeyBytes = base64UrlDecode(publicKeyBase64);
938
+ const publicKey = await crypto.subtle.importKey(
939
+ "spki",
940
+ publicKeyBytes.buffer,
941
+ {
942
+ name: "ECDSA",
943
+ namedCurve: "P-256"
944
+ },
945
+ false,
946
+ ["verify"]
947
+ );
948
+ const message = `${signedPayload.timestamp}.${signedPayload.payload}`;
949
+ const messageBytes = new TextEncoder().encode(message);
950
+ const signatureBytes = base64UrlDecode(signedPayload.signature);
951
+ return await crypto.subtle.verify(
952
+ {
953
+ name: "ECDSA",
954
+ hash: "SHA-256"
955
+ },
956
+ publicKey,
957
+ signatureBytes.buffer,
958
+ messageBytes
959
+ );
960
+ } catch {
961
+ return false;
962
+ }
963
+ }
964
+ var EPHEMERAL_KEY_STORAGE_KEY = "kentucky_signer_ephemeral";
965
+ var MemoryEphemeralKeyStorage = class {
966
+ constructor() {
967
+ this.keyPair = null;
968
+ }
969
+ async save(keyPair) {
970
+ this.keyPair = keyPair;
971
+ }
972
+ async load() {
973
+ return this.keyPair;
974
+ }
975
+ async clear() {
976
+ this.keyPair = null;
977
+ }
978
+ };
979
+ var IndexedDBEphemeralKeyStorage = class {
980
+ constructor() {
981
+ this.dbName = "kentucky_signer_ephemeral_keys";
982
+ this.storeName = "keys";
983
+ }
984
+ async getDB() {
985
+ return new Promise((resolve, reject) => {
986
+ const request = indexedDB.open(this.dbName, 1);
987
+ request.onerror = () => reject(request.error);
988
+ request.onsuccess = () => resolve(request.result);
989
+ request.onupgradeneeded = () => {
990
+ const db = request.result;
991
+ if (!db.objectStoreNames.contains(this.storeName)) {
992
+ db.createObjectStore(this.storeName);
993
+ }
994
+ };
995
+ });
996
+ }
997
+ async save(keyPair) {
998
+ const db = await this.getDB();
999
+ return new Promise((resolve, reject) => {
1000
+ const tx = db.transaction(this.storeName, "readwrite");
1001
+ const store = tx.objectStore(this.storeName);
1002
+ const data = {
1003
+ publicKey: keyPair.publicKey,
1004
+ privateKey: keyPair.privateKey,
1005
+ algorithm: keyPair.algorithm,
1006
+ createdAt: keyPair.createdAt
1007
+ };
1008
+ const request = store.put(data, EPHEMERAL_KEY_STORAGE_KEY);
1009
+ request.onerror = () => reject(request.error);
1010
+ request.onsuccess = () => resolve();
1011
+ });
1012
+ }
1013
+ async load() {
1014
+ const db = await this.getDB();
1015
+ return new Promise((resolve, reject) => {
1016
+ const tx = db.transaction(this.storeName, "readonly");
1017
+ const store = tx.objectStore(this.storeName);
1018
+ const request = store.get(EPHEMERAL_KEY_STORAGE_KEY);
1019
+ request.onerror = () => reject(request.error);
1020
+ request.onsuccess = () => {
1021
+ if (request.result) {
1022
+ resolve({
1023
+ publicKey: request.result.publicKey,
1024
+ privateKey: request.result.privateKey,
1025
+ algorithm: request.result.algorithm,
1026
+ createdAt: request.result.createdAt
1027
+ });
1028
+ } else {
1029
+ resolve(null);
1030
+ }
1031
+ };
1032
+ });
1033
+ }
1034
+ async clear() {
1035
+ const db = await this.getDB();
1036
+ return new Promise((resolve, reject) => {
1037
+ const tx = db.transaction(this.storeName, "readwrite");
1038
+ const store = tx.objectStore(this.storeName);
1039
+ const request = store.delete(EPHEMERAL_KEY_STORAGE_KEY);
1040
+ request.onerror = () => reject(request.error);
1041
+ request.onsuccess = () => resolve();
1042
+ });
1043
+ }
1044
+ };
1045
+ var EphemeralKeyManager = class {
1046
+ constructor(storage) {
1047
+ this.keyPair = null;
1048
+ this.storage = storage ?? new MemoryEphemeralKeyStorage();
1049
+ }
1050
+ /**
1051
+ * Get or generate ephemeral key pair
1052
+ */
1053
+ async getKeyPair() {
1054
+ if (!this.keyPair) {
1055
+ this.keyPair = await this.storage.load();
1056
+ }
1057
+ if (!this.keyPair) {
1058
+ this.keyPair = await generateEphemeralKeyPair();
1059
+ await this.storage.save(this.keyPair);
1060
+ }
1061
+ return this.keyPair;
1062
+ }
1063
+ /**
1064
+ * Get the public key for authentication
1065
+ */
1066
+ async getPublicKey() {
1067
+ const keyPair = await this.getKeyPair();
1068
+ return keyPair.publicKey;
1069
+ }
1070
+ /**
1071
+ * Sign a request payload
1072
+ */
1073
+ async signPayload(payload) {
1074
+ const keyPair = await this.getKeyPair();
1075
+ return signPayload(payload, keyPair);
1076
+ }
1077
+ /**
1078
+ * Rotate the key pair (generate new keys)
1079
+ */
1080
+ async rotate() {
1081
+ await this.storage.clear();
1082
+ this.keyPair = await generateEphemeralKeyPair();
1083
+ await this.storage.save(this.keyPair);
1084
+ return this.keyPair;
1085
+ }
1086
+ /**
1087
+ * Clear the key pair (logout)
1088
+ */
1089
+ async clear() {
1090
+ await this.storage.clear();
1091
+ this.keyPair = null;
1092
+ }
1093
+ /**
1094
+ * Check if a key pair exists
1095
+ */
1096
+ async hasKeyPair() {
1097
+ if (this.keyPair) return true;
1098
+ const loaded = await this.storage.load();
1099
+ return loaded !== null;
1100
+ }
1101
+ /**
1102
+ * Migrate key pair to a new storage backend
1103
+ *
1104
+ * This preserves the existing key pair while switching storage.
1105
+ * The key is saved to the new storage and removed from the old storage.
1106
+ *
1107
+ * @param newStorage - The new storage backend to migrate to
1108
+ */
1109
+ async migrateStorage(newStorage) {
1110
+ const currentKeyPair = this.keyPair ?? await this.storage.load();
1111
+ await this.storage.clear();
1112
+ this.storage = newStorage;
1113
+ if (currentKeyPair) {
1114
+ await this.storage.save(currentKeyPair);
1115
+ this.keyPair = currentKeyPair;
1116
+ }
1117
+ }
1118
+ /**
1119
+ * Get the current storage backend
1120
+ */
1121
+ getStorage() {
1122
+ return this.storage;
1123
+ }
1124
+ };
1125
+
1126
+ // src/secure-client.ts
1127
+ var SecureKentuckySignerClient = class {
1128
+ constructor(options) {
1129
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
1130
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
1131
+ this.timeout = options.timeout ?? 3e4;
1132
+ this.keyManager = options.ephemeralKeyManager ?? new EphemeralKeyManager(
1133
+ options.ephemeralKeyStorage ?? new MemoryEphemeralKeyStorage()
1134
+ );
1135
+ this.requireSigning = options.requireEphemeralSigning ?? true;
1136
+ }
1137
+ /**
1138
+ * Get the ephemeral key manager for advanced usage
1139
+ */
1140
+ getKeyManager() {
1141
+ return this.keyManager;
1142
+ }
1143
+ /**
1144
+ * Make a signed request to the API
1145
+ */
1146
+ async request(path, options = {}) {
1147
+ const { token, skipSigning, ...fetchOptions } = options;
1148
+ const headers = {
1149
+ "Content-Type": "application/json",
1150
+ ...options.headers
1151
+ };
1152
+ if (token) {
1153
+ ;
1154
+ headers["Authorization"] = `Bearer ${token}`;
1155
+ }
1156
+ let body = options.body;
1157
+ if (token && !skipSigning && this.requireSigning && body) {
1158
+ const signedPayload = await this.keyManager.signPayload(body);
1159
+ headers["X-Ephemeral-Signature"] = signedPayload.signature;
1160
+ headers["X-Ephemeral-Timestamp"] = signedPayload.timestamp.toString();
1161
+ }
1162
+ const controller = new AbortController();
1163
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1164
+ try {
1165
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
1166
+ ...fetchOptions,
1167
+ headers,
1168
+ body,
1169
+ signal: controller.signal
1170
+ });
1171
+ const data = await response.json();
1172
+ if (!response.ok || data.success === false) {
1173
+ const error = data;
1174
+ throw new KentuckySignerError(
1175
+ error.error?.message ?? "Unknown error",
1176
+ error.error?.code ?? "UNKNOWN_ERROR",
1177
+ error.error?.details
1178
+ );
1179
+ }
1180
+ return data;
1181
+ } finally {
1182
+ clearTimeout(timeoutId);
1183
+ }
1184
+ }
1185
+ /**
1186
+ * Get a challenge for passkey authentication
1187
+ */
1188
+ async getChallenge(accountId) {
1189
+ return this.request("/api/auth/challenge", {
1190
+ method: "POST",
1191
+ body: JSON.stringify({ account_id: accountId }),
1192
+ skipSigning: true
1193
+ // No token yet
1194
+ });
1195
+ }
1196
+ /**
1197
+ * Authenticate with a passkey credential
1198
+ *
1199
+ * Automatically sends the ephemeral public key for binding.
1200
+ */
1201
+ async authenticatePasskey(accountId, credential) {
1202
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1203
+ return this.request("/api/auth/passkey", {
1204
+ method: "POST",
1205
+ body: JSON.stringify({
1206
+ account_id: accountId,
1207
+ credential_id: credential.credentialId,
1208
+ client_data_json: credential.clientDataJSON,
1209
+ authenticator_data: credential.authenticatorData,
1210
+ signature: credential.signature,
1211
+ user_handle: credential.userHandle,
1212
+ ephemeral_public_key: ephemeralPublicKey
1213
+ }),
1214
+ skipSigning: true
1215
+ // No token yet
1216
+ });
1217
+ }
1218
+ /**
1219
+ * Authenticate with password
1220
+ *
1221
+ * Automatically sends the ephemeral public key for binding.
1222
+ */
1223
+ async authenticatePassword(request) {
1224
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1225
+ return this.request("/api/auth/password", {
1226
+ method: "POST",
1227
+ body: JSON.stringify({
1228
+ ...request,
1229
+ ephemeral_public_key: ephemeralPublicKey
1230
+ }),
1231
+ skipSigning: true
1232
+ // No token yet
1233
+ });
1234
+ }
1235
+ /**
1236
+ * Refresh an authentication token
1237
+ *
1238
+ * Generates a new ephemeral key pair and binds it to the new token.
1239
+ */
1240
+ async refreshToken(token) {
1241
+ await this.keyManager.rotate();
1242
+ const ephemeralPublicKey = await this.keyManager.getPublicKey();
1243
+ return this.request("/api/auth/refresh", {
1244
+ method: "POST",
1245
+ token,
1246
+ body: JSON.stringify({
1247
+ ephemeral_public_key: ephemeralPublicKey
1248
+ }),
1249
+ skipSigning: true
1250
+ // Use old token, but send new key
1251
+ });
1252
+ }
1253
+ /**
1254
+ * Logout and invalidate token
1255
+ *
1256
+ * Clears the ephemeral key pair.
1257
+ */
1258
+ async logout(token) {
1259
+ await this.request("/api/auth/logout", {
1260
+ method: "POST",
1261
+ token,
1262
+ skipSigning: true
1263
+ });
1264
+ await this.keyManager.clear();
1265
+ }
1266
+ /**
1267
+ * Get account information (signed request)
1268
+ */
1269
+ async getAccountInfo(accountId, token) {
1270
+ return this.request(`/api/accounts/${accountId}`, {
1271
+ method: "GET",
1272
+ token,
1273
+ skipSigning: true
1274
+ });
1275
+ }
1276
+ /**
1277
+ * Get extended account information (signed request)
1278
+ */
1279
+ async getAccountInfoExtended(accountId, token) {
1280
+ return this.request(
1281
+ `/api/accounts/${accountId}`,
1282
+ {
1283
+ method: "GET",
1284
+ token,
1285
+ skipSigning: true
1286
+ }
1287
+ );
1288
+ }
1289
+ /**
1290
+ * Sign an EVM transaction hash (signed request)
1291
+ */
1292
+ async signEvmTransaction(request, token) {
1293
+ return this.request("/api/sign/evm", {
1294
+ method: "POST",
1295
+ token,
1296
+ body: JSON.stringify(request)
1297
+ });
1298
+ }
1299
+ /**
1300
+ * Sign an EVM transaction hash with 2FA (signed request)
1301
+ */
1302
+ async signEvmTransactionWith2FA(request, token) {
1303
+ return this.request("/api/sign/evm", {
1304
+ method: "POST",
1305
+ token,
1306
+ body: JSON.stringify(request)
1307
+ });
1308
+ }
1309
+ /**
1310
+ * Sign a raw hash for EVM (signed request)
1311
+ */
1312
+ async signHash(hash, chainId, token) {
1313
+ const response = await this.signEvmTransaction(
1314
+ { tx_hash: hash, chain_id: chainId },
1315
+ token
1316
+ );
1317
+ return response.signature.full;
1318
+ }
1319
+ /**
1320
+ * Add password to account (signed request)
1321
+ */
1322
+ async addPassword(accountId, request, token) {
1323
+ return this.request(
1324
+ `/api/accounts/${accountId}/password`,
1325
+ {
1326
+ method: "POST",
1327
+ token,
1328
+ body: JSON.stringify(request)
1329
+ }
1330
+ );
1331
+ }
1332
+ /**
1333
+ * Add passkey to account (signed request)
1334
+ */
1335
+ async addPasskey(accountId, request, token) {
1336
+ return this.request(
1337
+ `/api/accounts/${accountId}/passkeys`,
1338
+ {
1339
+ method: "POST",
1340
+ token,
1341
+ body: JSON.stringify(request)
1342
+ }
1343
+ );
1344
+ }
1345
+ /**
1346
+ * Remove passkey from account (signed request)
1347
+ */
1348
+ async removePasskey(accountId, passkeyIndex, token) {
1349
+ return this.request(
1350
+ `/api/accounts/${accountId}/passkeys/${passkeyIndex}`,
1351
+ {
1352
+ method: "DELETE",
1353
+ token,
1354
+ body: JSON.stringify({ passkey_index: passkeyIndex })
1355
+ }
1356
+ );
1357
+ }
1358
+ /**
1359
+ * Create account with passkey (public endpoint, no signing)
1360
+ */
1361
+ async createAccountWithPasskey(attestationObject, label) {
1362
+ const body = {
1363
+ attestation_object: attestationObject
1364
+ };
1365
+ if (label) {
1366
+ body.label = label;
1367
+ }
1368
+ return this.request("/api/accounts/create/passkey", {
1369
+ method: "POST",
1370
+ body: JSON.stringify(body),
1371
+ skipSigning: true
1372
+ });
1373
+ }
1374
+ /**
1375
+ * Create account with password (public endpoint, no signing)
1376
+ */
1377
+ async createAccountWithPassword(request) {
1378
+ return this.request("/api/accounts/create/password", {
1379
+ method: "POST",
1380
+ body: JSON.stringify(request),
1381
+ skipSigning: true
1382
+ });
1383
+ }
1384
+ /**
1385
+ * Health check (public endpoint)
1386
+ */
1387
+ async healthCheck() {
1388
+ try {
1389
+ const response = await this.request("/api/health", {
1390
+ method: "GET",
1391
+ skipSigning: true
1392
+ });
1393
+ return response.status === "ok";
1394
+ } catch {
1395
+ return false;
1396
+ }
1397
+ }
1398
+ };
1399
+ function createSecureClient(options) {
1400
+ return new SecureKentuckySignerClient(options);
1401
+ }
1402
+
491
1403
  // src/auth.ts
492
1404
  function isWebAuthnAvailable() {
493
1405
  return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
@@ -582,7 +1494,8 @@ async function authenticateWithPasskey(options) {
582
1494
  const passkeyCredential = credentialToPasskey(credential);
583
1495
  const authResponse = await client.authenticatePasskey(
584
1496
  options.accountId,
585
- passkeyCredential
1497
+ passkeyCredential,
1498
+ options.ephemeralPublicKey
586
1499
  );
587
1500
  const accountInfo = await client.getAccountInfo(
588
1501
  options.accountId,
@@ -670,6 +1583,7 @@ async function registerPasskey(options) {
670
1583
  credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
671
1584
  clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
672
1585
  authenticatorData: base64UrlEncode(new Uint8Array(response.getAuthenticatorData())),
1586
+ attestationObject: base64UrlEncode(new Uint8Array(response.attestationObject)),
673
1587
  signature: "",
674
1588
  // Not applicable for registration
675
1589
  publicKey: base64UrlEncode(new Uint8Array(publicKeyBytes))
@@ -692,10 +1606,14 @@ async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
692
1606
  }
693
1607
  async function authenticateWithPassword(options) {
694
1608
  const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
695
- const authResponse = await client.authenticatePassword({
1609
+ const authRequest = {
696
1610
  account_id: options.accountId,
697
1611
  password: options.password
698
- });
1612
+ };
1613
+ if (options.ephemeralPublicKey) {
1614
+ authRequest.ephemeral_public_key = options.ephemeralPublicKey;
1615
+ }
1616
+ const authResponse = await client.authenticatePassword(authRequest);
699
1617
  const accountInfo = await client.getAccountInfo(
700
1618
  options.accountId,
701
1619
  authResponse.token
@@ -731,10 +1649,14 @@ async function createAccountWithPassword(options) {
731
1649
  }
732
1650
  // Annotate the CommonJS export names for ESM import in node:
733
1651
  0 && (module.exports = {
1652
+ EphemeralKeyManager,
1653
+ IndexedDBEphemeralKeyStorage,
734
1654
  KentuckySignerClient,
735
1655
  KentuckySignerError,
736
1656
  LocalStorageTokenStorage,
1657
+ MemoryEphemeralKeyStorage,
737
1658
  MemoryTokenStorage,
1659
+ SecureKentuckySignerClient,
738
1660
  authenticateWithPasskey,
739
1661
  authenticateWithPassword,
740
1662
  authenticateWithToken,
@@ -744,17 +1666,22 @@ async function createAccountWithPassword(options) {
744
1666
  createAccountWithPassword,
745
1667
  createClient,
746
1668
  createKentuckySignerAccount,
1669
+ createSecureClient,
747
1670
  createServerAccount,
748
1671
  formatError,
1672
+ generateEphemeralKeyPair,
749
1673
  getJwtExpiration,
750
1674
  hexToBytes,
751
1675
  isSessionValid,
752
1676
  isValidAccountId,
753
1677
  isValidEvmAddress,
754
1678
  isWebAuthnAvailable,
1679
+ isWebCryptoAvailable,
755
1680
  parseJwt,
756
1681
  refreshSessionIfNeeded,
757
1682
  registerPasskey,
1683
+ signPayload,
1684
+ verifyPayload,
758
1685
  withRetry
759
1686
  });
760
1687
  //# sourceMappingURL=index.js.map