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/README.md +195 -218
- package/dist/index.d.mts +802 -7
- package/dist/index.d.ts +802 -7
- package/dist/index.js +964 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +955 -37
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +61 -3
- package/dist/react/index.d.ts +61 -3
- package/dist/react/index.js +1286 -173
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1288 -174
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/account.ts +111 -22
- package/src/auth.ts +16 -6
- package/src/client.ts +438 -18
- package/src/ephemeral.ts +407 -0
- package/src/index.ts +56 -0
- package/src/react/context.tsx +360 -45
- package/src/react/hooks.ts +11 -0
- package/src/react/index.ts +1 -0
- package/src/secure-client.ts +417 -0
- package/src/types.ts +332 -0
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
|
|
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(
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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 =
|
|
696
|
+
yParity = v >= 27 ? v - 27 : v;
|
|
308
697
|
} else {
|
|
309
|
-
yParity =
|
|
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
|
|
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
|