kentucky-signer-viem 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -219
- 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.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
|
|
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(
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
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 =
|
|
750
|
+
yParity = v >= 27 ? v - 27 : v;
|
|
353
751
|
} else {
|
|
354
|
-
yParity =
|
|
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
|
|
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
|