kentucky-signer-viem 0.1.1 → 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/src/client.ts CHANGED
@@ -3,13 +3,35 @@ import type {
3
3
  ChallengeResponse,
4
4
  AuthResponse,
5
5
  AccountInfoResponse,
6
+ AccountInfoExtendedResponse,
6
7
  EvmSignatureResponse,
7
8
  ApiErrorResponse,
8
9
  PasskeyCredential,
9
10
  AccountCreationResponse,
10
11
  SignEvmRequest,
12
+ SignEvmRequestWith2FA,
11
13
  CreatePasswordAccountRequest,
12
14
  PasswordAuthRequest,
15
+ AddPasswordRequest,
16
+ AddPasswordResponse,
17
+ AddPasskeyRequest,
18
+ AddPasskeyResponse,
19
+ RemovePasskeyResponse,
20
+ AddGuardianRequest,
21
+ AddGuardianResponse,
22
+ RemoveGuardianResponse,
23
+ GetGuardiansResponse,
24
+ InitiateRecoveryResponse,
25
+ VerifyGuardianRequest,
26
+ VerifyGuardianResponse,
27
+ RecoveryStatusResponse,
28
+ CompleteRecoveryResponse,
29
+ CancelRecoveryResponse,
30
+ TwoFactorStatusResponse,
31
+ TotpSetupResponse,
32
+ TwoFactorResponse,
33
+ TwoFactorVerifyResponse,
34
+ PinSetupResponse,
13
35
  } from './types'
14
36
  import type { Hex } from 'viem'
15
37
 
@@ -40,7 +62,8 @@ export class KentuckySignerClient {
40
62
 
41
63
  constructor(options: ClientOptions) {
42
64
  this.baseUrl = options.baseUrl.replace(/\/$/, '') // Remove trailing slash
43
- this.fetchImpl = options.fetch ?? globalThis.fetch
65
+ // Bind fetch to globalThis to avoid "Illegal invocation" in browsers
66
+ this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis)
44
67
  this.timeout = options.timeout ?? 30000
45
68
  }
46
69
 
@@ -107,22 +130,30 @@ export class KentuckySignerClient {
107
130
  *
108
131
  * @param accountId - Account ID to authenticate
109
132
  * @param credential - WebAuthn credential from navigator.credentials.get()
133
+ * @param ephemeralPublicKey - Optional ephemeral public key for secure mode binding
110
134
  * @returns Authentication response with JWT token
111
135
  */
112
136
  async authenticatePasskey(
113
137
  accountId: string,
114
- credential: PasskeyCredential
138
+ credential: PasskeyCredential,
139
+ ephemeralPublicKey?: string
115
140
  ): Promise<AuthResponse> {
141
+ const body: Record<string, string | undefined> = {
142
+ account_id: accountId,
143
+ credential_id: credential.credentialId,
144
+ client_data_json: credential.clientDataJSON,
145
+ authenticator_data: credential.authenticatorData,
146
+ signature: credential.signature,
147
+ user_handle: credential.userHandle,
148
+ }
149
+
150
+ if (ephemeralPublicKey) {
151
+ body.ephemeral_public_key = ephemeralPublicKey
152
+ }
153
+
116
154
  return this.request<AuthResponse>('/api/auth/passkey', {
117
155
  method: 'POST',
118
- body: JSON.stringify({
119
- account_id: accountId,
120
- credential_id: credential.credentialId,
121
- client_data_json: credential.clientDataJSON,
122
- authenticator_data: credential.authenticatorData,
123
- signature: credential.signature,
124
- user_handle: credential.userHandle,
125
- }),
156
+ body: JSON.stringify(body),
126
157
  })
127
158
  }
128
159
 
@@ -223,20 +254,23 @@ export class KentuckySignerClient {
223
254
  /**
224
255
  * Create a new account with passkey authentication
225
256
  *
226
- * @param credential - WebAuthn credential from navigator.credentials.create()
257
+ * @param attestationObject - Base64url encoded attestation object from WebAuthn
258
+ * @param label - Optional label for the passkey (defaults to "Owner Passkey")
227
259
  * @returns Account creation response with account ID and addresses
228
260
  */
229
261
  async createAccountWithPasskey(
230
- credential: PasskeyCredential & { publicKey: string }
262
+ attestationObject: string,
263
+ label?: string
231
264
  ): Promise<AccountCreationResponse> {
265
+ const body: Record<string, string> = {
266
+ attestation_object: attestationObject,
267
+ }
268
+ if (label) {
269
+ body.label = label
270
+ }
232
271
  return this.request<AccountCreationResponse>('/api/accounts/create/passkey', {
233
272
  method: 'POST',
234
- body: JSON.stringify({
235
- credential_id: credential.credentialId,
236
- public_key: credential.publicKey,
237
- client_data_json: credential.clientDataJSON,
238
- authenticator_data: credential.authenticatorData,
239
- }),
273
+ body: JSON.stringify(body),
240
274
  })
241
275
  }
242
276
 
@@ -268,6 +302,84 @@ export class KentuckySignerClient {
268
302
  })
269
303
  }
270
304
 
305
+ /**
306
+ * Add password authentication to an existing account
307
+ *
308
+ * Enables password-based authentication for an account that was created
309
+ * with passkey-only authentication.
310
+ *
311
+ * @param accountId - Account ID
312
+ * @param request - Password and confirmation
313
+ * @param token - JWT token
314
+ * @returns Success response
315
+ */
316
+ async addPassword(
317
+ accountId: string,
318
+ request: AddPasswordRequest,
319
+ token: string
320
+ ): Promise<AddPasswordResponse> {
321
+ return this.request<AddPasswordResponse>(`/api/accounts/${accountId}/password`, {
322
+ method: 'POST',
323
+ token,
324
+ body: JSON.stringify(request),
325
+ })
326
+ }
327
+
328
+ /**
329
+ * Get extended account information including auth config
330
+ *
331
+ * @param accountId - Account ID
332
+ * @param token - JWT token
333
+ * @returns Extended account info with auth config and passkey count
334
+ */
335
+ async getAccountInfoExtended(accountId: string, token: string): Promise<AccountInfoExtendedResponse> {
336
+ return this.request<AccountInfoExtendedResponse>(`/api/accounts/${accountId}`, {
337
+ method: 'GET',
338
+ token,
339
+ })
340
+ }
341
+
342
+ /**
343
+ * Add a recovery passkey to an existing account
344
+ *
345
+ * @param accountId - Account ID
346
+ * @param request - Passkey data (COSE public key, credential ID, algorithm, label)
347
+ * @param token - JWT token
348
+ * @returns Success response with label
349
+ */
350
+ async addPasskey(
351
+ accountId: string,
352
+ request: AddPasskeyRequest,
353
+ token: string
354
+ ): Promise<AddPasskeyResponse> {
355
+ return this.request<AddPasskeyResponse>(`/api/accounts/${accountId}/passkeys`, {
356
+ method: 'POST',
357
+ token,
358
+ body: JSON.stringify(request),
359
+ })
360
+ }
361
+
362
+ /**
363
+ * Remove a passkey from an account
364
+ *
365
+ * Note: Cannot remove the owner passkey (index 0)
366
+ *
367
+ * @param accountId - Account ID
368
+ * @param passkeyIndex - Index of passkey to remove (1-3 for recovery passkeys)
369
+ * @param token - JWT token
370
+ * @returns Success response
371
+ */
372
+ async removePasskey(
373
+ accountId: string,
374
+ passkeyIndex: number,
375
+ token: string
376
+ ): Promise<RemovePasskeyResponse> {
377
+ return this.request<RemovePasskeyResponse>(`/api/accounts/${accountId}/passkeys/${passkeyIndex}`, {
378
+ method: 'DELETE',
379
+ token,
380
+ })
381
+ }
382
+
271
383
  /**
272
384
  * Health check
273
385
  *
@@ -295,6 +407,314 @@ export class KentuckySignerClient {
295
407
  })
296
408
  return response.version
297
409
  }
410
+
411
+ // ============================================================================
412
+ // Guardian Management
413
+ // ============================================================================
414
+
415
+ /**
416
+ * Add a guardian passkey to an account
417
+ *
418
+ * Guardians can participate in account recovery but cannot access the wallet.
419
+ * An account can have up to 3 guardians (indices 1-3).
420
+ *
421
+ * @param request - Guardian data (attestation object and optional label)
422
+ * @param token - JWT token
423
+ * @returns Success response with guardian index and count
424
+ */
425
+ async addGuardian(
426
+ request: AddGuardianRequest,
427
+ token: string
428
+ ): Promise<AddGuardianResponse> {
429
+ return this.request<AddGuardianResponse>('/api/guardians/add', {
430
+ method: 'POST',
431
+ token,
432
+ body: JSON.stringify(request),
433
+ })
434
+ }
435
+
436
+ /**
437
+ * Remove a guardian passkey from an account
438
+ *
439
+ * Cannot remove guardians during an active recovery.
440
+ *
441
+ * @param guardianIndex - Index of guardian to remove (1-3)
442
+ * @param token - JWT token
443
+ * @returns Success response with remaining guardian count
444
+ */
445
+ async removeGuardian(
446
+ guardianIndex: number,
447
+ token: string
448
+ ): Promise<RemoveGuardianResponse> {
449
+ return this.request<RemoveGuardianResponse>('/api/guardians/remove', {
450
+ method: 'POST',
451
+ token,
452
+ body: JSON.stringify({ guardian_index: guardianIndex }),
453
+ })
454
+ }
455
+
456
+ /**
457
+ * Get guardians for an account
458
+ *
459
+ * @param token - JWT token
460
+ * @returns Guardian list with indices and labels
461
+ */
462
+ async getGuardians(token: string): Promise<GetGuardiansResponse> {
463
+ return this.request<GetGuardiansResponse>('/api/guardians', {
464
+ method: 'GET',
465
+ token,
466
+ })
467
+ }
468
+
469
+ // ============================================================================
470
+ // Account Recovery
471
+ // ============================================================================
472
+
473
+ /**
474
+ * Initiate account recovery
475
+ *
476
+ * Call this when you've lost access to your account. You'll need to register
477
+ * a new passkey which will become the new owner passkey after recovery completes.
478
+ *
479
+ * @param accountId - Account ID to recover
480
+ * @param attestationObject - WebAuthn attestation object for new owner passkey
481
+ * @param label - Optional label for new owner passkey
482
+ * @returns Challenges for guardians to sign, threshold, and timelock info
483
+ */
484
+ async initiateRecovery(
485
+ accountId: string,
486
+ attestationObject: string,
487
+ label?: string
488
+ ): Promise<InitiateRecoveryResponse> {
489
+ const body: Record<string, string> = {
490
+ account_id: accountId,
491
+ attestation_object: attestationObject,
492
+ }
493
+ if (label) {
494
+ body.label = label
495
+ }
496
+ return this.request<InitiateRecoveryResponse>('/api/recovery/initiate', {
497
+ method: 'POST',
498
+ body: JSON.stringify(body),
499
+ })
500
+ }
501
+
502
+ /**
503
+ * Submit a guardian signature for recovery
504
+ *
505
+ * Each guardian must sign their challenge using their passkey.
506
+ *
507
+ * @param request - Guardian signature data
508
+ * @returns Current verification status
509
+ */
510
+ async verifyGuardian(request: VerifyGuardianRequest): Promise<VerifyGuardianResponse> {
511
+ return this.request<VerifyGuardianResponse>('/api/recovery/verify', {
512
+ method: 'POST',
513
+ body: JSON.stringify(request),
514
+ })
515
+ }
516
+
517
+ /**
518
+ * Get recovery status for an account
519
+ *
520
+ * @param accountId - Account ID to check
521
+ * @returns Recovery status including verification count and timelock
522
+ */
523
+ async getRecoveryStatus(accountId: string): Promise<RecoveryStatusResponse> {
524
+ return this.request<RecoveryStatusResponse>('/api/recovery/status', {
525
+ method: 'POST',
526
+ body: JSON.stringify({ account_id: accountId }),
527
+ })
528
+ }
529
+
530
+ /**
531
+ * Complete account recovery
532
+ *
533
+ * Call this after enough guardians have verified and the timelock has expired.
534
+ * The new owner passkey will replace the old one.
535
+ *
536
+ * @param accountId - Account ID to complete recovery for
537
+ * @returns Success message
538
+ */
539
+ async completeRecovery(accountId: string): Promise<CompleteRecoveryResponse> {
540
+ return this.request<CompleteRecoveryResponse>('/api/recovery/complete', {
541
+ method: 'POST',
542
+ body: JSON.stringify({ account_id: accountId }),
543
+ })
544
+ }
545
+
546
+ /**
547
+ * Cancel a pending recovery
548
+ *
549
+ * Only the current owner (with valid auth) can cancel a recovery.
550
+ * Use this if you regain access and want to stop an unauthorized recovery.
551
+ *
552
+ * @param token - JWT token (must be authenticated as owner)
553
+ * @returns Success message
554
+ */
555
+ async cancelRecovery(token: string): Promise<CancelRecoveryResponse> {
556
+ return this.request<CancelRecoveryResponse>('/api/recovery/cancel', {
557
+ method: 'POST',
558
+ token,
559
+ })
560
+ }
561
+
562
+ // ============================================================================
563
+ // Two-Factor Authentication (2FA)
564
+ // ============================================================================
565
+
566
+ /**
567
+ * Get 2FA status for the authenticated account
568
+ *
569
+ * @param token - JWT token
570
+ * @returns 2FA status including TOTP and PIN enablement
571
+ */
572
+ async get2FAStatus(token: string): Promise<TwoFactorStatusResponse> {
573
+ return this.request<TwoFactorStatusResponse>('/api/2fa/status', {
574
+ method: 'GET',
575
+ token,
576
+ })
577
+ }
578
+
579
+ /**
580
+ * Setup TOTP for the account
581
+ *
582
+ * Returns an otpauth:// URI for QR code generation and a base32 secret
583
+ * for manual entry. After setup, call enableTOTP with a valid code to
584
+ * complete the setup.
585
+ *
586
+ * @param token - JWT token
587
+ * @returns TOTP setup response with URI and secret
588
+ */
589
+ async setupTOTP(token: string): Promise<TotpSetupResponse> {
590
+ return this.request<TotpSetupResponse>('/api/2fa/totp/setup', {
591
+ method: 'POST',
592
+ token,
593
+ })
594
+ }
595
+
596
+ /**
597
+ * Verify and enable TOTP for the account
598
+ *
599
+ * Call this after setupTOTP with a valid code from the authenticator app.
600
+ *
601
+ * @param code - 6-digit TOTP code from authenticator app
602
+ * @param token - JWT token
603
+ * @returns Success response
604
+ */
605
+ async enableTOTP(code: string, token: string): Promise<TwoFactorResponse> {
606
+ return this.request<TwoFactorResponse>('/api/2fa/totp/enable', {
607
+ method: 'POST',
608
+ token,
609
+ body: JSON.stringify({ code }),
610
+ })
611
+ }
612
+
613
+ /**
614
+ * Disable TOTP for the account
615
+ *
616
+ * Requires a valid TOTP code for confirmation.
617
+ *
618
+ * @param code - Current 6-digit TOTP code
619
+ * @param token - JWT token
620
+ * @returns Success response
621
+ */
622
+ async disableTOTP(code: string, token: string): Promise<TwoFactorResponse> {
623
+ return this.request<TwoFactorResponse>('/api/2fa/totp/disable', {
624
+ method: 'POST',
625
+ token,
626
+ body: JSON.stringify({ code }),
627
+ })
628
+ }
629
+
630
+ /**
631
+ * Verify a TOTP code
632
+ *
633
+ * Use this to verify a TOTP code without performing any other operation.
634
+ *
635
+ * @param code - 6-digit TOTP code
636
+ * @param token - JWT token
637
+ * @returns Verification result
638
+ */
639
+ async verifyTOTP(code: string, token: string): Promise<TwoFactorVerifyResponse> {
640
+ return this.request<TwoFactorVerifyResponse>('/api/2fa/totp/verify', {
641
+ method: 'POST',
642
+ token,
643
+ body: JSON.stringify({ code }),
644
+ })
645
+ }
646
+
647
+ /**
648
+ * Setup PIN for the account
649
+ *
650
+ * Sets a 4-digit or 6-digit PIN. If a PIN is already set, this replaces it.
651
+ *
652
+ * @param pin - 4 or 6 digit PIN
653
+ * @param token - JWT token
654
+ * @returns Success response with PIN length
655
+ */
656
+ async setupPIN(pin: string, token: string): Promise<PinSetupResponse> {
657
+ return this.request<PinSetupResponse>('/api/2fa/pin/setup', {
658
+ method: 'POST',
659
+ token,
660
+ body: JSON.stringify({ pin }),
661
+ })
662
+ }
663
+
664
+ /**
665
+ * Disable PIN for the account
666
+ *
667
+ * Requires the current PIN for confirmation.
668
+ *
669
+ * @param pin - Current PIN
670
+ * @param token - JWT token
671
+ * @returns Success response
672
+ */
673
+ async disablePIN(pin: string, token: string): Promise<TwoFactorResponse> {
674
+ return this.request<TwoFactorResponse>('/api/2fa/pin/disable', {
675
+ method: 'POST',
676
+ token,
677
+ body: JSON.stringify({ pin }),
678
+ })
679
+ }
680
+
681
+ /**
682
+ * Verify a PIN
683
+ *
684
+ * Use this to verify a PIN without performing any other operation.
685
+ *
686
+ * @param pin - PIN to verify
687
+ * @param token - JWT token
688
+ * @returns Verification result
689
+ */
690
+ async verifyPIN(pin: string, token: string): Promise<TwoFactorVerifyResponse> {
691
+ return this.request<TwoFactorVerifyResponse>('/api/2fa/pin/verify', {
692
+ method: 'POST',
693
+ token,
694
+ body: JSON.stringify({ pin }),
695
+ })
696
+ }
697
+
698
+ /**
699
+ * Sign an EVM transaction with 2FA
700
+ *
701
+ * Use this method when 2FA is enabled. If 2FA is not enabled,
702
+ * you can use the regular signEvmTransaction method instead.
703
+ *
704
+ * @param request - Sign request including tx_hash, chain_id, and optional 2FA codes
705
+ * @param token - JWT token
706
+ * @returns Signature response with r, s, v components
707
+ */
708
+ async signEvmTransactionWith2FA(
709
+ request: SignEvmRequestWith2FA,
710
+ token: string
711
+ ): Promise<EvmSignatureResponse> {
712
+ return this.request<EvmSignatureResponse>('/api/sign/evm', {
713
+ method: 'POST',
714
+ token,
715
+ body: JSON.stringify(request),
716
+ })
717
+ }
298
718
  }
299
719
 
300
720
  /**