kentucky-signer-viem 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/react/index.mjs
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
useState,
|
|
6
6
|
useCallback,
|
|
7
7
|
useEffect,
|
|
8
|
-
useMemo
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef
|
|
9
10
|
} from "react";
|
|
10
11
|
|
|
11
12
|
// src/client.ts
|
|
@@ -20,7 +21,7 @@ var KentuckySignerError = class extends Error {
|
|
|
20
21
|
var KentuckySignerClient = class {
|
|
21
22
|
constructor(options) {
|
|
22
23
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
23
|
-
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
24
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
24
25
|
this.timeout = options.timeout ?? 3e4;
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
@@ -75,9 +76,831 @@ var KentuckySignerClient = class {
|
|
|
75
76
|
*
|
|
76
77
|
* @param accountId - Account ID to authenticate
|
|
77
78
|
* @param credential - WebAuthn credential from navigator.credentials.get()
|
|
79
|
+
* @param ephemeralPublicKey - Optional ephemeral public key for secure mode binding
|
|
78
80
|
* @returns Authentication response with JWT token
|
|
79
81
|
*/
|
|
82
|
+
async authenticatePasskey(accountId, credential, ephemeralPublicKey) {
|
|
83
|
+
const body = {
|
|
84
|
+
account_id: accountId,
|
|
85
|
+
credential_id: credential.credentialId,
|
|
86
|
+
client_data_json: credential.clientDataJSON,
|
|
87
|
+
authenticator_data: credential.authenticatorData,
|
|
88
|
+
signature: credential.signature,
|
|
89
|
+
user_handle: credential.userHandle
|
|
90
|
+
};
|
|
91
|
+
if (ephemeralPublicKey) {
|
|
92
|
+
body.ephemeral_public_key = ephemeralPublicKey;
|
|
93
|
+
}
|
|
94
|
+
return this.request("/api/auth/passkey", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: JSON.stringify(body)
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Refresh an authentication token
|
|
101
|
+
*
|
|
102
|
+
* @param token - Current JWT token
|
|
103
|
+
* @returns New authentication response with fresh token
|
|
104
|
+
*/
|
|
105
|
+
async refreshToken(token) {
|
|
106
|
+
return this.request("/api/auth/refresh", {
|
|
107
|
+
method: "POST",
|
|
108
|
+
token
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Logout and invalidate token
|
|
113
|
+
*
|
|
114
|
+
* @param token - JWT token to invalidate
|
|
115
|
+
*/
|
|
116
|
+
async logout(token) {
|
|
117
|
+
await this.request("/api/auth/logout", {
|
|
118
|
+
method: "POST",
|
|
119
|
+
token
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get account information
|
|
124
|
+
*
|
|
125
|
+
* @param accountId - Account ID
|
|
126
|
+
* @param token - JWT token
|
|
127
|
+
* @returns Account info with addresses and passkeys
|
|
128
|
+
*/
|
|
129
|
+
async getAccountInfo(accountId, token) {
|
|
130
|
+
return this.request(`/api/accounts/${accountId}`, {
|
|
131
|
+
method: "GET",
|
|
132
|
+
token
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if an account exists
|
|
137
|
+
*
|
|
138
|
+
* @param accountId - Account ID
|
|
139
|
+
* @param token - JWT token
|
|
140
|
+
* @returns True if account exists
|
|
141
|
+
*/
|
|
142
|
+
async accountExists(accountId, token) {
|
|
143
|
+
try {
|
|
144
|
+
await this.request(`/api/accounts/${accountId}`, {
|
|
145
|
+
method: "HEAD",
|
|
146
|
+
token
|
|
147
|
+
});
|
|
148
|
+
return true;
|
|
149
|
+
} catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Sign an EVM transaction hash
|
|
155
|
+
*
|
|
156
|
+
* @param request - Sign request with tx_hash and chain_id
|
|
157
|
+
* @param token - JWT token
|
|
158
|
+
* @returns Signature response with r, s, v components
|
|
159
|
+
*/
|
|
160
|
+
async signEvmTransaction(request, token) {
|
|
161
|
+
return this.request("/api/sign/evm", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
token,
|
|
164
|
+
body: JSON.stringify(request)
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Sign a raw hash for EVM
|
|
169
|
+
*
|
|
170
|
+
* Convenience method that wraps signEvmTransaction.
|
|
171
|
+
*
|
|
172
|
+
* @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
|
|
173
|
+
* @param chainId - Chain ID
|
|
174
|
+
* @param token - JWT token
|
|
175
|
+
* @returns Full signature (hex encoded with 0x prefix)
|
|
176
|
+
*/
|
|
177
|
+
async signHash(hash, chainId, token) {
|
|
178
|
+
const response = await this.signEvmTransaction(
|
|
179
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
180
|
+
token
|
|
181
|
+
);
|
|
182
|
+
return response.signature.full;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create a new account with passkey authentication
|
|
186
|
+
*
|
|
187
|
+
* @param attestationObject - Base64url encoded attestation object from WebAuthn
|
|
188
|
+
* @param label - Optional label for the passkey (defaults to "Owner Passkey")
|
|
189
|
+
* @returns Account creation response with account ID and addresses
|
|
190
|
+
*/
|
|
191
|
+
async createAccountWithPasskey(attestationObject, label) {
|
|
192
|
+
const body = {
|
|
193
|
+
attestation_object: attestationObject
|
|
194
|
+
};
|
|
195
|
+
if (label) {
|
|
196
|
+
body.label = label;
|
|
197
|
+
}
|
|
198
|
+
return this.request("/api/accounts/create/passkey", {
|
|
199
|
+
method: "POST",
|
|
200
|
+
body: JSON.stringify(body)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create a new account with password authentication
|
|
205
|
+
*
|
|
206
|
+
* @param request - Password and confirmation
|
|
207
|
+
* @returns Account creation response with account ID and addresses
|
|
208
|
+
*/
|
|
209
|
+
async createAccountWithPassword(request) {
|
|
210
|
+
return this.request("/api/accounts/create/password", {
|
|
211
|
+
method: "POST",
|
|
212
|
+
body: JSON.stringify(request)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Authenticate with password
|
|
217
|
+
*
|
|
218
|
+
* @param request - Account ID and password
|
|
219
|
+
* @returns Authentication response with JWT token
|
|
220
|
+
*/
|
|
221
|
+
async authenticatePassword(request) {
|
|
222
|
+
return this.request("/api/auth/password", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: JSON.stringify(request)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Add password authentication to an existing account
|
|
229
|
+
*
|
|
230
|
+
* Enables password-based authentication for an account that was created
|
|
231
|
+
* with passkey-only authentication.
|
|
232
|
+
*
|
|
233
|
+
* @param accountId - Account ID
|
|
234
|
+
* @param request - Password and confirmation
|
|
235
|
+
* @param token - JWT token
|
|
236
|
+
* @returns Success response
|
|
237
|
+
*/
|
|
238
|
+
async addPassword(accountId, request, token) {
|
|
239
|
+
return this.request(`/api/accounts/${accountId}/password`, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
token,
|
|
242
|
+
body: JSON.stringify(request)
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get extended account information including auth config
|
|
247
|
+
*
|
|
248
|
+
* @param accountId - Account ID
|
|
249
|
+
* @param token - JWT token
|
|
250
|
+
* @returns Extended account info with auth config and passkey count
|
|
251
|
+
*/
|
|
252
|
+
async getAccountInfoExtended(accountId, token) {
|
|
253
|
+
return this.request(`/api/accounts/${accountId}`, {
|
|
254
|
+
method: "GET",
|
|
255
|
+
token
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Add a recovery passkey to an existing account
|
|
260
|
+
*
|
|
261
|
+
* @param accountId - Account ID
|
|
262
|
+
* @param request - Passkey data (COSE public key, credential ID, algorithm, label)
|
|
263
|
+
* @param token - JWT token
|
|
264
|
+
* @returns Success response with label
|
|
265
|
+
*/
|
|
266
|
+
async addPasskey(accountId, request, token) {
|
|
267
|
+
return this.request(`/api/accounts/${accountId}/passkeys`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
token,
|
|
270
|
+
body: JSON.stringify(request)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Remove a passkey from an account
|
|
275
|
+
*
|
|
276
|
+
* Note: Cannot remove the owner passkey (index 0)
|
|
277
|
+
*
|
|
278
|
+
* @param accountId - Account ID
|
|
279
|
+
* @param passkeyIndex - Index of passkey to remove (1-3 for recovery passkeys)
|
|
280
|
+
* @param token - JWT token
|
|
281
|
+
* @returns Success response
|
|
282
|
+
*/
|
|
283
|
+
async removePasskey(accountId, passkeyIndex, token) {
|
|
284
|
+
return this.request(`/api/accounts/${accountId}/passkeys/${passkeyIndex}`, {
|
|
285
|
+
method: "DELETE",
|
|
286
|
+
token
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Health check
|
|
291
|
+
*
|
|
292
|
+
* @returns True if the API is healthy
|
|
293
|
+
*/
|
|
294
|
+
async healthCheck() {
|
|
295
|
+
try {
|
|
296
|
+
const response = await this.request("/api/health", {
|
|
297
|
+
method: "GET"
|
|
298
|
+
});
|
|
299
|
+
return response.status === "ok";
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Get API version
|
|
306
|
+
*
|
|
307
|
+
* @returns Version string
|
|
308
|
+
*/
|
|
309
|
+
async getVersion() {
|
|
310
|
+
const response = await this.request("/api/version", {
|
|
311
|
+
method: "GET"
|
|
312
|
+
});
|
|
313
|
+
return response.version;
|
|
314
|
+
}
|
|
315
|
+
// ============================================================================
|
|
316
|
+
// Guardian Management
|
|
317
|
+
// ============================================================================
|
|
318
|
+
/**
|
|
319
|
+
* Add a guardian passkey to an account
|
|
320
|
+
*
|
|
321
|
+
* Guardians can participate in account recovery but cannot access the wallet.
|
|
322
|
+
* An account can have up to 3 guardians (indices 1-3).
|
|
323
|
+
*
|
|
324
|
+
* @param request - Guardian data (attestation object and optional label)
|
|
325
|
+
* @param token - JWT token
|
|
326
|
+
* @returns Success response with guardian index and count
|
|
327
|
+
*/
|
|
328
|
+
async addGuardian(request, token) {
|
|
329
|
+
return this.request("/api/guardians/add", {
|
|
330
|
+
method: "POST",
|
|
331
|
+
token,
|
|
332
|
+
body: JSON.stringify(request)
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Remove a guardian passkey from an account
|
|
337
|
+
*
|
|
338
|
+
* Cannot remove guardians during an active recovery.
|
|
339
|
+
*
|
|
340
|
+
* @param guardianIndex - Index of guardian to remove (1-3)
|
|
341
|
+
* @param token - JWT token
|
|
342
|
+
* @returns Success response with remaining guardian count
|
|
343
|
+
*/
|
|
344
|
+
async removeGuardian(guardianIndex, token) {
|
|
345
|
+
return this.request("/api/guardians/remove", {
|
|
346
|
+
method: "POST",
|
|
347
|
+
token,
|
|
348
|
+
body: JSON.stringify({ guardian_index: guardianIndex })
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get guardians for an account
|
|
353
|
+
*
|
|
354
|
+
* @param token - JWT token
|
|
355
|
+
* @returns Guardian list with indices and labels
|
|
356
|
+
*/
|
|
357
|
+
async getGuardians(token) {
|
|
358
|
+
return this.request("/api/guardians", {
|
|
359
|
+
method: "GET",
|
|
360
|
+
token
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// ============================================================================
|
|
364
|
+
// Account Recovery
|
|
365
|
+
// ============================================================================
|
|
366
|
+
/**
|
|
367
|
+
* Initiate account recovery
|
|
368
|
+
*
|
|
369
|
+
* Call this when you've lost access to your account. You'll need to register
|
|
370
|
+
* a new passkey which will become the new owner passkey after recovery completes.
|
|
371
|
+
*
|
|
372
|
+
* @param accountId - Account ID to recover
|
|
373
|
+
* @param attestationObject - WebAuthn attestation object for new owner passkey
|
|
374
|
+
* @param label - Optional label for new owner passkey
|
|
375
|
+
* @returns Challenges for guardians to sign, threshold, and timelock info
|
|
376
|
+
*/
|
|
377
|
+
async initiateRecovery(accountId, attestationObject, label) {
|
|
378
|
+
const body = {
|
|
379
|
+
account_id: accountId,
|
|
380
|
+
attestation_object: attestationObject
|
|
381
|
+
};
|
|
382
|
+
if (label) {
|
|
383
|
+
body.label = label;
|
|
384
|
+
}
|
|
385
|
+
return this.request("/api/recovery/initiate", {
|
|
386
|
+
method: "POST",
|
|
387
|
+
body: JSON.stringify(body)
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Submit a guardian signature for recovery
|
|
392
|
+
*
|
|
393
|
+
* Each guardian must sign their challenge using their passkey.
|
|
394
|
+
*
|
|
395
|
+
* @param request - Guardian signature data
|
|
396
|
+
* @returns Current verification status
|
|
397
|
+
*/
|
|
398
|
+
async verifyGuardian(request) {
|
|
399
|
+
return this.request("/api/recovery/verify", {
|
|
400
|
+
method: "POST",
|
|
401
|
+
body: JSON.stringify(request)
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get recovery status for an account
|
|
406
|
+
*
|
|
407
|
+
* @param accountId - Account ID to check
|
|
408
|
+
* @returns Recovery status including verification count and timelock
|
|
409
|
+
*/
|
|
410
|
+
async getRecoveryStatus(accountId) {
|
|
411
|
+
return this.request("/api/recovery/status", {
|
|
412
|
+
method: "POST",
|
|
413
|
+
body: JSON.stringify({ account_id: accountId })
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Complete account recovery
|
|
418
|
+
*
|
|
419
|
+
* Call this after enough guardians have verified and the timelock has expired.
|
|
420
|
+
* The new owner passkey will replace the old one.
|
|
421
|
+
*
|
|
422
|
+
* @param accountId - Account ID to complete recovery for
|
|
423
|
+
* @returns Success message
|
|
424
|
+
*/
|
|
425
|
+
async completeRecovery(accountId) {
|
|
426
|
+
return this.request("/api/recovery/complete", {
|
|
427
|
+
method: "POST",
|
|
428
|
+
body: JSON.stringify({ account_id: accountId })
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Cancel a pending recovery
|
|
433
|
+
*
|
|
434
|
+
* Only the current owner (with valid auth) can cancel a recovery.
|
|
435
|
+
* Use this if you regain access and want to stop an unauthorized recovery.
|
|
436
|
+
*
|
|
437
|
+
* @param token - JWT token (must be authenticated as owner)
|
|
438
|
+
* @returns Success message
|
|
439
|
+
*/
|
|
440
|
+
async cancelRecovery(token) {
|
|
441
|
+
return this.request("/api/recovery/cancel", {
|
|
442
|
+
method: "POST",
|
|
443
|
+
token
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
// ============================================================================
|
|
447
|
+
// Two-Factor Authentication (2FA)
|
|
448
|
+
// ============================================================================
|
|
449
|
+
/**
|
|
450
|
+
* Get 2FA status for the authenticated account
|
|
451
|
+
*
|
|
452
|
+
* @param token - JWT token
|
|
453
|
+
* @returns 2FA status including TOTP and PIN enablement
|
|
454
|
+
*/
|
|
455
|
+
async get2FAStatus(token) {
|
|
456
|
+
return this.request("/api/2fa/status", {
|
|
457
|
+
method: "GET",
|
|
458
|
+
token
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Setup TOTP for the account
|
|
463
|
+
*
|
|
464
|
+
* Returns an otpauth:// URI for QR code generation and a base32 secret
|
|
465
|
+
* for manual entry. After setup, call enableTOTP with a valid code to
|
|
466
|
+
* complete the setup.
|
|
467
|
+
*
|
|
468
|
+
* @param token - JWT token
|
|
469
|
+
* @returns TOTP setup response with URI and secret
|
|
470
|
+
*/
|
|
471
|
+
async setupTOTP(token) {
|
|
472
|
+
return this.request("/api/2fa/totp/setup", {
|
|
473
|
+
method: "POST",
|
|
474
|
+
token
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Verify and enable TOTP for the account
|
|
479
|
+
*
|
|
480
|
+
* Call this after setupTOTP with a valid code from the authenticator app.
|
|
481
|
+
*
|
|
482
|
+
* @param code - 6-digit TOTP code from authenticator app
|
|
483
|
+
* @param token - JWT token
|
|
484
|
+
* @returns Success response
|
|
485
|
+
*/
|
|
486
|
+
async enableTOTP(code, token) {
|
|
487
|
+
return this.request("/api/2fa/totp/enable", {
|
|
488
|
+
method: "POST",
|
|
489
|
+
token,
|
|
490
|
+
body: JSON.stringify({ code })
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Disable TOTP for the account
|
|
495
|
+
*
|
|
496
|
+
* Requires a valid TOTP code for confirmation.
|
|
497
|
+
*
|
|
498
|
+
* @param code - Current 6-digit TOTP code
|
|
499
|
+
* @param token - JWT token
|
|
500
|
+
* @returns Success response
|
|
501
|
+
*/
|
|
502
|
+
async disableTOTP(code, token) {
|
|
503
|
+
return this.request("/api/2fa/totp/disable", {
|
|
504
|
+
method: "POST",
|
|
505
|
+
token,
|
|
506
|
+
body: JSON.stringify({ code })
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Verify a TOTP code
|
|
511
|
+
*
|
|
512
|
+
* Use this to verify a TOTP code without performing any other operation.
|
|
513
|
+
*
|
|
514
|
+
* @param code - 6-digit TOTP code
|
|
515
|
+
* @param token - JWT token
|
|
516
|
+
* @returns Verification result
|
|
517
|
+
*/
|
|
518
|
+
async verifyTOTP(code, token) {
|
|
519
|
+
return this.request("/api/2fa/totp/verify", {
|
|
520
|
+
method: "POST",
|
|
521
|
+
token,
|
|
522
|
+
body: JSON.stringify({ code })
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Setup PIN for the account
|
|
527
|
+
*
|
|
528
|
+
* Sets a 4-digit or 6-digit PIN. If a PIN is already set, this replaces it.
|
|
529
|
+
*
|
|
530
|
+
* @param pin - 4 or 6 digit PIN
|
|
531
|
+
* @param token - JWT token
|
|
532
|
+
* @returns Success response with PIN length
|
|
533
|
+
*/
|
|
534
|
+
async setupPIN(pin, token) {
|
|
535
|
+
return this.request("/api/2fa/pin/setup", {
|
|
536
|
+
method: "POST",
|
|
537
|
+
token,
|
|
538
|
+
body: JSON.stringify({ pin })
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Disable PIN for the account
|
|
543
|
+
*
|
|
544
|
+
* Requires the current PIN for confirmation.
|
|
545
|
+
*
|
|
546
|
+
* @param pin - Current PIN
|
|
547
|
+
* @param token - JWT token
|
|
548
|
+
* @returns Success response
|
|
549
|
+
*/
|
|
550
|
+
async disablePIN(pin, token) {
|
|
551
|
+
return this.request("/api/2fa/pin/disable", {
|
|
552
|
+
method: "POST",
|
|
553
|
+
token,
|
|
554
|
+
body: JSON.stringify({ pin })
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Verify a PIN
|
|
559
|
+
*
|
|
560
|
+
* Use this to verify a PIN without performing any other operation.
|
|
561
|
+
*
|
|
562
|
+
* @param pin - PIN to verify
|
|
563
|
+
* @param token - JWT token
|
|
564
|
+
* @returns Verification result
|
|
565
|
+
*/
|
|
566
|
+
async verifyPIN(pin, token) {
|
|
567
|
+
return this.request("/api/2fa/pin/verify", {
|
|
568
|
+
method: "POST",
|
|
569
|
+
token,
|
|
570
|
+
body: JSON.stringify({ pin })
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Sign an EVM transaction with 2FA
|
|
575
|
+
*
|
|
576
|
+
* Use this method when 2FA is enabled. If 2FA is not enabled,
|
|
577
|
+
* you can use the regular signEvmTransaction method instead.
|
|
578
|
+
*
|
|
579
|
+
* @param request - Sign request including tx_hash, chain_id, and optional 2FA codes
|
|
580
|
+
* @param token - JWT token
|
|
581
|
+
* @returns Signature response with r, s, v components
|
|
582
|
+
*/
|
|
583
|
+
async signEvmTransactionWith2FA(request, token) {
|
|
584
|
+
return this.request("/api/sign/evm", {
|
|
585
|
+
method: "POST",
|
|
586
|
+
token,
|
|
587
|
+
body: JSON.stringify(request)
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/utils.ts
|
|
593
|
+
function base64UrlEncode(data) {
|
|
594
|
+
let base64;
|
|
595
|
+
if (typeof Buffer !== "undefined") {
|
|
596
|
+
base64 = Buffer.from(data).toString("base64");
|
|
597
|
+
} else {
|
|
598
|
+
const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
|
|
599
|
+
base64 = btoa(binary);
|
|
600
|
+
}
|
|
601
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
602
|
+
}
|
|
603
|
+
function base64UrlDecode(str) {
|
|
604
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
605
|
+
const padding = (4 - base64.length % 4) % 4;
|
|
606
|
+
base64 += "=".repeat(padding);
|
|
607
|
+
if (typeof Buffer !== "undefined") {
|
|
608
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
609
|
+
} else {
|
|
610
|
+
const binary = atob(base64);
|
|
611
|
+
const bytes = new Uint8Array(binary.length);
|
|
612
|
+
for (let i = 0; i < binary.length; i++) {
|
|
613
|
+
bytes[i] = binary.charCodeAt(i);
|
|
614
|
+
}
|
|
615
|
+
return bytes;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/ephemeral.ts
|
|
620
|
+
function isWebCryptoAvailable() {
|
|
621
|
+
return typeof crypto !== "undefined" && typeof crypto.subtle !== "undefined" && typeof crypto.getRandomValues !== "undefined";
|
|
622
|
+
}
|
|
623
|
+
async function generateEphemeralKeyPair() {
|
|
624
|
+
if (!isWebCryptoAvailable()) {
|
|
625
|
+
throw new Error("WebCrypto is not available in this environment");
|
|
626
|
+
}
|
|
627
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
628
|
+
{
|
|
629
|
+
name: "ECDSA",
|
|
630
|
+
namedCurve: "P-256"
|
|
631
|
+
},
|
|
632
|
+
true,
|
|
633
|
+
// extractable (only for public key export)
|
|
634
|
+
["sign", "verify"]
|
|
635
|
+
);
|
|
636
|
+
const publicKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
|
637
|
+
const publicKeyBase64 = base64UrlEncode(new Uint8Array(publicKeyBuffer));
|
|
638
|
+
return {
|
|
639
|
+
publicKey: publicKeyBase64,
|
|
640
|
+
privateKey: keyPair.privateKey,
|
|
641
|
+
algorithm: "ES256",
|
|
642
|
+
createdAt: Date.now()
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
async function signPayload(payload, keyPair) {
|
|
646
|
+
const payloadString = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
647
|
+
const timestamp = Date.now();
|
|
648
|
+
const message = `${timestamp}.${payloadString}`;
|
|
649
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
650
|
+
const signatureBuffer = await crypto.subtle.sign(
|
|
651
|
+
{
|
|
652
|
+
name: "ECDSA",
|
|
653
|
+
hash: "SHA-256"
|
|
654
|
+
},
|
|
655
|
+
keyPair.privateKey,
|
|
656
|
+
messageBytes
|
|
657
|
+
);
|
|
658
|
+
const signatureBase64 = base64UrlEncode(new Uint8Array(signatureBuffer));
|
|
659
|
+
return {
|
|
660
|
+
payload: payloadString,
|
|
661
|
+
signature: signatureBase64,
|
|
662
|
+
timestamp
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
var EPHEMERAL_KEY_STORAGE_KEY = "kentucky_signer_ephemeral";
|
|
666
|
+
var MemoryEphemeralKeyStorage = class {
|
|
667
|
+
constructor() {
|
|
668
|
+
this.keyPair = null;
|
|
669
|
+
}
|
|
670
|
+
async save(keyPair) {
|
|
671
|
+
this.keyPair = keyPair;
|
|
672
|
+
}
|
|
673
|
+
async load() {
|
|
674
|
+
return this.keyPair;
|
|
675
|
+
}
|
|
676
|
+
async clear() {
|
|
677
|
+
this.keyPair = null;
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
var IndexedDBEphemeralKeyStorage = class {
|
|
681
|
+
constructor() {
|
|
682
|
+
this.dbName = "kentucky_signer_ephemeral_keys";
|
|
683
|
+
this.storeName = "keys";
|
|
684
|
+
}
|
|
685
|
+
async getDB() {
|
|
686
|
+
return new Promise((resolve, reject) => {
|
|
687
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
688
|
+
request.onerror = () => reject(request.error);
|
|
689
|
+
request.onsuccess = () => resolve(request.result);
|
|
690
|
+
request.onupgradeneeded = () => {
|
|
691
|
+
const db = request.result;
|
|
692
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
693
|
+
db.createObjectStore(this.storeName);
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
async save(keyPair) {
|
|
699
|
+
const db = await this.getDB();
|
|
700
|
+
return new Promise((resolve, reject) => {
|
|
701
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
702
|
+
const store = tx.objectStore(this.storeName);
|
|
703
|
+
const data = {
|
|
704
|
+
publicKey: keyPair.publicKey,
|
|
705
|
+
privateKey: keyPair.privateKey,
|
|
706
|
+
algorithm: keyPair.algorithm,
|
|
707
|
+
createdAt: keyPair.createdAt
|
|
708
|
+
};
|
|
709
|
+
const request = store.put(data, EPHEMERAL_KEY_STORAGE_KEY);
|
|
710
|
+
request.onerror = () => reject(request.error);
|
|
711
|
+
request.onsuccess = () => resolve();
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
async load() {
|
|
715
|
+
const db = await this.getDB();
|
|
716
|
+
return new Promise((resolve, reject) => {
|
|
717
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
718
|
+
const store = tx.objectStore(this.storeName);
|
|
719
|
+
const request = store.get(EPHEMERAL_KEY_STORAGE_KEY);
|
|
720
|
+
request.onerror = () => reject(request.error);
|
|
721
|
+
request.onsuccess = () => {
|
|
722
|
+
if (request.result) {
|
|
723
|
+
resolve({
|
|
724
|
+
publicKey: request.result.publicKey,
|
|
725
|
+
privateKey: request.result.privateKey,
|
|
726
|
+
algorithm: request.result.algorithm,
|
|
727
|
+
createdAt: request.result.createdAt
|
|
728
|
+
});
|
|
729
|
+
} else {
|
|
730
|
+
resolve(null);
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
async clear() {
|
|
736
|
+
const db = await this.getDB();
|
|
737
|
+
return new Promise((resolve, reject) => {
|
|
738
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
739
|
+
const store = tx.objectStore(this.storeName);
|
|
740
|
+
const request = store.delete(EPHEMERAL_KEY_STORAGE_KEY);
|
|
741
|
+
request.onerror = () => reject(request.error);
|
|
742
|
+
request.onsuccess = () => resolve();
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var EphemeralKeyManager = class {
|
|
747
|
+
constructor(storage) {
|
|
748
|
+
this.keyPair = null;
|
|
749
|
+
this.storage = storage ?? new MemoryEphemeralKeyStorage();
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Get or generate ephemeral key pair
|
|
753
|
+
*/
|
|
754
|
+
async getKeyPair() {
|
|
755
|
+
if (!this.keyPair) {
|
|
756
|
+
this.keyPair = await this.storage.load();
|
|
757
|
+
}
|
|
758
|
+
if (!this.keyPair) {
|
|
759
|
+
this.keyPair = await generateEphemeralKeyPair();
|
|
760
|
+
await this.storage.save(this.keyPair);
|
|
761
|
+
}
|
|
762
|
+
return this.keyPair;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Get the public key for authentication
|
|
766
|
+
*/
|
|
767
|
+
async getPublicKey() {
|
|
768
|
+
const keyPair = await this.getKeyPair();
|
|
769
|
+
return keyPair.publicKey;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Sign a request payload
|
|
773
|
+
*/
|
|
774
|
+
async signPayload(payload) {
|
|
775
|
+
const keyPair = await this.getKeyPair();
|
|
776
|
+
return signPayload(payload, keyPair);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Rotate the key pair (generate new keys)
|
|
780
|
+
*/
|
|
781
|
+
async rotate() {
|
|
782
|
+
await this.storage.clear();
|
|
783
|
+
this.keyPair = await generateEphemeralKeyPair();
|
|
784
|
+
await this.storage.save(this.keyPair);
|
|
785
|
+
return this.keyPair;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Clear the key pair (logout)
|
|
789
|
+
*/
|
|
790
|
+
async clear() {
|
|
791
|
+
await this.storage.clear();
|
|
792
|
+
this.keyPair = null;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Check if a key pair exists
|
|
796
|
+
*/
|
|
797
|
+
async hasKeyPair() {
|
|
798
|
+
if (this.keyPair) return true;
|
|
799
|
+
const loaded = await this.storage.load();
|
|
800
|
+
return loaded !== null;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Migrate key pair to a new storage backend
|
|
804
|
+
*
|
|
805
|
+
* This preserves the existing key pair while switching storage.
|
|
806
|
+
* The key is saved to the new storage and removed from the old storage.
|
|
807
|
+
*
|
|
808
|
+
* @param newStorage - The new storage backend to migrate to
|
|
809
|
+
*/
|
|
810
|
+
async migrateStorage(newStorage) {
|
|
811
|
+
const currentKeyPair = this.keyPair ?? await this.storage.load();
|
|
812
|
+
await this.storage.clear();
|
|
813
|
+
this.storage = newStorage;
|
|
814
|
+
if (currentKeyPair) {
|
|
815
|
+
await this.storage.save(currentKeyPair);
|
|
816
|
+
this.keyPair = currentKeyPair;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Get the current storage backend
|
|
821
|
+
*/
|
|
822
|
+
getStorage() {
|
|
823
|
+
return this.storage;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// src/secure-client.ts
|
|
828
|
+
var SecureKentuckySignerClient = class {
|
|
829
|
+
constructor(options) {
|
|
830
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
831
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
832
|
+
this.timeout = options.timeout ?? 3e4;
|
|
833
|
+
this.keyManager = options.ephemeralKeyManager ?? new EphemeralKeyManager(
|
|
834
|
+
options.ephemeralKeyStorage ?? new MemoryEphemeralKeyStorage()
|
|
835
|
+
);
|
|
836
|
+
this.requireSigning = options.requireEphemeralSigning ?? true;
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Get the ephemeral key manager for advanced usage
|
|
840
|
+
*/
|
|
841
|
+
getKeyManager() {
|
|
842
|
+
return this.keyManager;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Make a signed request to the API
|
|
846
|
+
*/
|
|
847
|
+
async request(path, options = {}) {
|
|
848
|
+
const { token, skipSigning, ...fetchOptions } = options;
|
|
849
|
+
const headers = {
|
|
850
|
+
"Content-Type": "application/json",
|
|
851
|
+
...options.headers
|
|
852
|
+
};
|
|
853
|
+
if (token) {
|
|
854
|
+
;
|
|
855
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
856
|
+
}
|
|
857
|
+
let body = options.body;
|
|
858
|
+
if (token && !skipSigning && this.requireSigning && body) {
|
|
859
|
+
const signedPayload = await this.keyManager.signPayload(body);
|
|
860
|
+
headers["X-Ephemeral-Signature"] = signedPayload.signature;
|
|
861
|
+
headers["X-Ephemeral-Timestamp"] = signedPayload.timestamp.toString();
|
|
862
|
+
}
|
|
863
|
+
const controller = new AbortController();
|
|
864
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
865
|
+
try {
|
|
866
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
867
|
+
...fetchOptions,
|
|
868
|
+
headers,
|
|
869
|
+
body,
|
|
870
|
+
signal: controller.signal
|
|
871
|
+
});
|
|
872
|
+
const data = await response.json();
|
|
873
|
+
if (!response.ok || data.success === false) {
|
|
874
|
+
const error = data;
|
|
875
|
+
throw new KentuckySignerError(
|
|
876
|
+
error.error?.message ?? "Unknown error",
|
|
877
|
+
error.error?.code ?? "UNKNOWN_ERROR",
|
|
878
|
+
error.error?.details
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return data;
|
|
882
|
+
} finally {
|
|
883
|
+
clearTimeout(timeoutId);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Get a challenge for passkey authentication
|
|
888
|
+
*/
|
|
889
|
+
async getChallenge(accountId) {
|
|
890
|
+
return this.request("/api/auth/challenge", {
|
|
891
|
+
method: "POST",
|
|
892
|
+
body: JSON.stringify({ account_id: accountId }),
|
|
893
|
+
skipSigning: true
|
|
894
|
+
// No token yet
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Authenticate with a passkey credential
|
|
899
|
+
*
|
|
900
|
+
* Automatically sends the ephemeral public key for binding.
|
|
901
|
+
*/
|
|
80
902
|
async authenticatePasskey(accountId, credential) {
|
|
903
|
+
const ephemeralPublicKey = await this.keyManager.getPublicKey();
|
|
81
904
|
return this.request("/api/auth/passkey", {
|
|
82
905
|
method: "POST",
|
|
83
906
|
body: JSON.stringify({
|
|
@@ -86,70 +909,86 @@ var KentuckySignerClient = class {
|
|
|
86
909
|
client_data_json: credential.clientDataJSON,
|
|
87
910
|
authenticator_data: credential.authenticatorData,
|
|
88
911
|
signature: credential.signature,
|
|
89
|
-
user_handle: credential.userHandle
|
|
90
|
-
|
|
912
|
+
user_handle: credential.userHandle,
|
|
913
|
+
ephemeral_public_key: ephemeralPublicKey
|
|
914
|
+
}),
|
|
915
|
+
skipSigning: true
|
|
916
|
+
// No token yet
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Authenticate with password
|
|
921
|
+
*
|
|
922
|
+
* Automatically sends the ephemeral public key for binding.
|
|
923
|
+
*/
|
|
924
|
+
async authenticatePassword(request) {
|
|
925
|
+
const ephemeralPublicKey = await this.keyManager.getPublicKey();
|
|
926
|
+
return this.request("/api/auth/password", {
|
|
927
|
+
method: "POST",
|
|
928
|
+
body: JSON.stringify({
|
|
929
|
+
...request,
|
|
930
|
+
ephemeral_public_key: ephemeralPublicKey
|
|
931
|
+
}),
|
|
932
|
+
skipSigning: true
|
|
933
|
+
// No token yet
|
|
91
934
|
});
|
|
92
935
|
}
|
|
93
936
|
/**
|
|
94
937
|
* Refresh an authentication token
|
|
95
938
|
*
|
|
96
|
-
*
|
|
97
|
-
* @returns New authentication response with fresh token
|
|
939
|
+
* Generates a new ephemeral key pair and binds it to the new token.
|
|
98
940
|
*/
|
|
99
941
|
async refreshToken(token) {
|
|
942
|
+
await this.keyManager.rotate();
|
|
943
|
+
const ephemeralPublicKey = await this.keyManager.getPublicKey();
|
|
100
944
|
return this.request("/api/auth/refresh", {
|
|
101
945
|
method: "POST",
|
|
102
|
-
token
|
|
946
|
+
token,
|
|
947
|
+
body: JSON.stringify({
|
|
948
|
+
ephemeral_public_key: ephemeralPublicKey
|
|
949
|
+
}),
|
|
950
|
+
skipSigning: true
|
|
951
|
+
// Use old token, but send new key
|
|
103
952
|
});
|
|
104
953
|
}
|
|
105
954
|
/**
|
|
106
955
|
* Logout and invalidate token
|
|
107
956
|
*
|
|
108
|
-
*
|
|
957
|
+
* Clears the ephemeral key pair.
|
|
109
958
|
*/
|
|
110
959
|
async logout(token) {
|
|
111
960
|
await this.request("/api/auth/logout", {
|
|
112
961
|
method: "POST",
|
|
113
|
-
token
|
|
962
|
+
token,
|
|
963
|
+
skipSigning: true
|
|
114
964
|
});
|
|
965
|
+
await this.keyManager.clear();
|
|
115
966
|
}
|
|
116
967
|
/**
|
|
117
|
-
* Get account information
|
|
118
|
-
*
|
|
119
|
-
* @param accountId - Account ID
|
|
120
|
-
* @param token - JWT token
|
|
121
|
-
* @returns Account info with addresses and passkeys
|
|
968
|
+
* Get account information (signed request)
|
|
122
969
|
*/
|
|
123
970
|
async getAccountInfo(accountId, token) {
|
|
124
971
|
return this.request(`/api/accounts/${accountId}`, {
|
|
125
972
|
method: "GET",
|
|
126
|
-
token
|
|
973
|
+
token,
|
|
974
|
+
skipSigning: true
|
|
127
975
|
});
|
|
128
976
|
}
|
|
129
977
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* @param accountId - Account ID
|
|
133
|
-
* @param token - JWT token
|
|
134
|
-
* @returns True if account exists
|
|
978
|
+
* Get extended account information (signed request)
|
|
135
979
|
*/
|
|
136
|
-
async
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
980
|
+
async getAccountInfoExtended(accountId, token) {
|
|
981
|
+
return this.request(
|
|
982
|
+
`/api/accounts/${accountId}`,
|
|
983
|
+
{
|
|
984
|
+
method: "GET",
|
|
985
|
+
token,
|
|
986
|
+
skipSigning: true
|
|
987
|
+
}
|
|
988
|
+
);
|
|
146
989
|
}
|
|
147
990
|
/**
|
|
148
|
-
* Sign an EVM transaction hash
|
|
149
|
-
*
|
|
150
|
-
* @param request - Sign request with tx_hash and chain_id
|
|
151
|
-
* @param token - JWT token
|
|
152
|
-
* @returns Signature response with r, s, v components
|
|
991
|
+
* Sign an EVM transaction hash (signed request)
|
|
153
992
|
*/
|
|
154
993
|
async signEvmTransaction(request, token) {
|
|
155
994
|
return this.request("/api/sign/evm", {
|
|
@@ -159,14 +998,17 @@ var KentuckySignerClient = class {
|
|
|
159
998
|
});
|
|
160
999
|
}
|
|
161
1000
|
/**
|
|
162
|
-
* Sign
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
1001
|
+
* Sign an EVM transaction hash with 2FA (signed request)
|
|
1002
|
+
*/
|
|
1003
|
+
async signEvmTransactionWith2FA(request, token) {
|
|
1004
|
+
return this.request("/api/sign/evm", {
|
|
1005
|
+
method: "POST",
|
|
1006
|
+
token,
|
|
1007
|
+
body: JSON.stringify(request)
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Sign a raw hash for EVM (signed request)
|
|
170
1012
|
*/
|
|
171
1013
|
async signHash(hash, chainId, token) {
|
|
172
1014
|
const response = await this.signEvmTransaction(
|
|
@@ -176,101 +1018,86 @@ var KentuckySignerClient = class {
|
|
|
176
1018
|
return response.signature.full;
|
|
177
1019
|
}
|
|
178
1020
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* @param credential - WebAuthn credential from navigator.credentials.create()
|
|
182
|
-
* @returns Account creation response with account ID and addresses
|
|
1021
|
+
* Add password to account (signed request)
|
|
183
1022
|
*/
|
|
184
|
-
async
|
|
185
|
-
return this.request(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
1023
|
+
async addPassword(accountId, request, token) {
|
|
1024
|
+
return this.request(
|
|
1025
|
+
`/api/accounts/${accountId}/password`,
|
|
1026
|
+
{
|
|
1027
|
+
method: "POST",
|
|
1028
|
+
token,
|
|
1029
|
+
body: JSON.stringify(request)
|
|
1030
|
+
}
|
|
1031
|
+
);
|
|
194
1032
|
}
|
|
195
1033
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* @param request - Password and confirmation
|
|
199
|
-
* @returns Account creation response with account ID and addresses
|
|
1034
|
+
* Add passkey to account (signed request)
|
|
200
1035
|
*/
|
|
201
|
-
async
|
|
202
|
-
return this.request(
|
|
1036
|
+
async addPasskey(accountId, request, token) {
|
|
1037
|
+
return this.request(
|
|
1038
|
+
`/api/accounts/${accountId}/passkeys`,
|
|
1039
|
+
{
|
|
1040
|
+
method: "POST",
|
|
1041
|
+
token,
|
|
1042
|
+
body: JSON.stringify(request)
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Remove passkey from account (signed request)
|
|
1048
|
+
*/
|
|
1049
|
+
async removePasskey(accountId, passkeyIndex, token) {
|
|
1050
|
+
return this.request(
|
|
1051
|
+
`/api/accounts/${accountId}/passkeys/${passkeyIndex}`,
|
|
1052
|
+
{
|
|
1053
|
+
method: "DELETE",
|
|
1054
|
+
token,
|
|
1055
|
+
body: JSON.stringify({ passkey_index: passkeyIndex })
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Create account with passkey (public endpoint, no signing)
|
|
1061
|
+
*/
|
|
1062
|
+
async createAccountWithPasskey(attestationObject, label) {
|
|
1063
|
+
const body = {
|
|
1064
|
+
attestation_object: attestationObject
|
|
1065
|
+
};
|
|
1066
|
+
if (label) {
|
|
1067
|
+
body.label = label;
|
|
1068
|
+
}
|
|
1069
|
+
return this.request("/api/accounts/create/passkey", {
|
|
203
1070
|
method: "POST",
|
|
204
|
-
body: JSON.stringify(
|
|
1071
|
+
body: JSON.stringify(body),
|
|
1072
|
+
skipSigning: true
|
|
205
1073
|
});
|
|
206
1074
|
}
|
|
207
1075
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* @param request - Account ID and password
|
|
211
|
-
* @returns Authentication response with JWT token
|
|
1076
|
+
* Create account with password (public endpoint, no signing)
|
|
212
1077
|
*/
|
|
213
|
-
async
|
|
214
|
-
return this.request("/api/
|
|
1078
|
+
async createAccountWithPassword(request) {
|
|
1079
|
+
return this.request("/api/accounts/create/password", {
|
|
215
1080
|
method: "POST",
|
|
216
|
-
body: JSON.stringify(request)
|
|
1081
|
+
body: JSON.stringify(request),
|
|
1082
|
+
skipSigning: true
|
|
217
1083
|
});
|
|
218
1084
|
}
|
|
219
1085
|
/**
|
|
220
|
-
* Health check
|
|
221
|
-
*
|
|
222
|
-
* @returns True if the API is healthy
|
|
1086
|
+
* Health check (public endpoint)
|
|
223
1087
|
*/
|
|
224
1088
|
async healthCheck() {
|
|
225
1089
|
try {
|
|
226
1090
|
const response = await this.request("/api/health", {
|
|
227
|
-
method: "GET"
|
|
1091
|
+
method: "GET",
|
|
1092
|
+
skipSigning: true
|
|
228
1093
|
});
|
|
229
1094
|
return response.status === "ok";
|
|
230
1095
|
} catch {
|
|
231
1096
|
return false;
|
|
232
1097
|
}
|
|
233
1098
|
}
|
|
234
|
-
/**
|
|
235
|
-
* Get API version
|
|
236
|
-
*
|
|
237
|
-
* @returns Version string
|
|
238
|
-
*/
|
|
239
|
-
async getVersion() {
|
|
240
|
-
const response = await this.request("/api/version", {
|
|
241
|
-
method: "GET"
|
|
242
|
-
});
|
|
243
|
-
return response.version;
|
|
244
|
-
}
|
|
245
1099
|
};
|
|
246
1100
|
|
|
247
|
-
// src/utils.ts
|
|
248
|
-
function base64UrlEncode(data) {
|
|
249
|
-
let base64;
|
|
250
|
-
if (typeof Buffer !== "undefined") {
|
|
251
|
-
base64 = Buffer.from(data).toString("base64");
|
|
252
|
-
} else {
|
|
253
|
-
const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
|
|
254
|
-
base64 = btoa(binary);
|
|
255
|
-
}
|
|
256
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
257
|
-
}
|
|
258
|
-
function base64UrlDecode(str) {
|
|
259
|
-
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
260
|
-
const padding = (4 - base64.length % 4) % 4;
|
|
261
|
-
base64 += "=".repeat(padding);
|
|
262
|
-
if (typeof Buffer !== "undefined") {
|
|
263
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
264
|
-
} else {
|
|
265
|
-
const binary = atob(base64);
|
|
266
|
-
const bytes = new Uint8Array(binary.length);
|
|
267
|
-
for (let i = 0; i < binary.length; i++) {
|
|
268
|
-
bytes[i] = binary.charCodeAt(i);
|
|
269
|
-
}
|
|
270
|
-
return bytes;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
1101
|
// src/auth.ts
|
|
275
1102
|
function isWebAuthnAvailable() {
|
|
276
1103
|
return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
|
|
@@ -345,7 +1172,8 @@ async function authenticateWithPasskey(options) {
|
|
|
345
1172
|
const passkeyCredential = credentialToPasskey(credential);
|
|
346
1173
|
const authResponse = await client.authenticatePasskey(
|
|
347
1174
|
options.accountId,
|
|
348
|
-
passkeyCredential
|
|
1175
|
+
passkeyCredential,
|
|
1176
|
+
options.ephemeralPublicKey
|
|
349
1177
|
);
|
|
350
1178
|
const accountInfo = await client.getAccountInfo(
|
|
351
1179
|
options.accountId,
|
|
@@ -376,6 +1204,30 @@ async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
|
|
|
376
1204
|
expiresAt: Date.now() + authResponse.expires_in * 1e3
|
|
377
1205
|
};
|
|
378
1206
|
}
|
|
1207
|
+
async function authenticateWithPassword(options) {
|
|
1208
|
+
const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
|
|
1209
|
+
const authRequest = {
|
|
1210
|
+
account_id: options.accountId,
|
|
1211
|
+
password: options.password
|
|
1212
|
+
};
|
|
1213
|
+
if (options.ephemeralPublicKey) {
|
|
1214
|
+
authRequest.ephemeral_public_key = options.ephemeralPublicKey;
|
|
1215
|
+
}
|
|
1216
|
+
const authResponse = await client.authenticatePassword(authRequest);
|
|
1217
|
+
const accountInfo = await client.getAccountInfo(
|
|
1218
|
+
options.accountId,
|
|
1219
|
+
authResponse.token
|
|
1220
|
+
);
|
|
1221
|
+
const expiresAt = Date.now() + authResponse.expires_in * 1e3;
|
|
1222
|
+
return {
|
|
1223
|
+
token: authResponse.token,
|
|
1224
|
+
accountId: options.accountId,
|
|
1225
|
+
evmAddress: accountInfo.addresses.evm,
|
|
1226
|
+
btcAddress: accountInfo.addresses.bitcoin,
|
|
1227
|
+
solAddress: accountInfo.addresses.solana,
|
|
1228
|
+
expiresAt
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
379
1231
|
|
|
380
1232
|
// src/account.ts
|
|
381
1233
|
import {
|
|
@@ -386,9 +1238,9 @@ import {
|
|
|
386
1238
|
} from "viem";
|
|
387
1239
|
import { toAccount } from "viem/accounts";
|
|
388
1240
|
function createKentuckySignerAccount(options) {
|
|
389
|
-
const { config, defaultChainId = 1, onSessionExpired } = options;
|
|
1241
|
+
const { config, defaultChainId = 1, onSessionExpired, secureClient, on2FARequired } = options;
|
|
390
1242
|
let session = options.session;
|
|
391
|
-
const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
|
|
1243
|
+
const client = secureClient ?? new KentuckySignerClient({ baseUrl: config.baseUrl });
|
|
392
1244
|
async function getToken() {
|
|
393
1245
|
if (Date.now() + 6e4 >= session.expiresAt) {
|
|
394
1246
|
if (onSessionExpired) {
|
|
@@ -405,17 +1257,63 @@ function createKentuckySignerAccount(options) {
|
|
|
405
1257
|
}
|
|
406
1258
|
async function signHash(hash, chainId) {
|
|
407
1259
|
const token = await getToken();
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1260
|
+
try {
|
|
1261
|
+
const response = await client.signEvmTransaction(
|
|
1262
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
1263
|
+
token
|
|
1264
|
+
);
|
|
1265
|
+
return response.signature.full;
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
|
|
1268
|
+
const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
|
|
1269
|
+
const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
|
|
1270
|
+
const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
|
|
1271
|
+
const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
|
|
1272
|
+
if (!codes) {
|
|
1273
|
+
throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
|
|
1274
|
+
}
|
|
1275
|
+
const response = await client.signEvmTransactionWith2FA(
|
|
1276
|
+
{ tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
|
|
1277
|
+
token
|
|
1278
|
+
);
|
|
1279
|
+
return response.signature.full;
|
|
1280
|
+
}
|
|
1281
|
+
throw err;
|
|
1282
|
+
}
|
|
413
1283
|
}
|
|
414
|
-
function
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
1284
|
+
async function signHashWithComponents(hash, chainId) {
|
|
1285
|
+
const token = await getToken();
|
|
1286
|
+
try {
|
|
1287
|
+
const response = await client.signEvmTransaction(
|
|
1288
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
1289
|
+
token
|
|
1290
|
+
);
|
|
1291
|
+
return {
|
|
1292
|
+
r: response.signature.r,
|
|
1293
|
+
s: response.signature.s,
|
|
1294
|
+
v: response.signature.v
|
|
1295
|
+
};
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
if (err instanceof KentuckySignerError && err.code === "2FA_REQUIRED" && on2FARequired) {
|
|
1298
|
+
const totpRequired = err.message.includes("TOTP") || (err.details?.includes("totp_code") ?? false);
|
|
1299
|
+
const pinRequired = err.message.includes("PIN") || (err.details?.includes("pin") ?? false);
|
|
1300
|
+
const pinLength = err.details?.match(/(\d)-digit/)?.[1] ? parseInt(err.details.match(/(\d)-digit/)[1]) : 6;
|
|
1301
|
+
const codes = await on2FARequired({ totpRequired, pinRequired, pinLength });
|
|
1302
|
+
if (!codes) {
|
|
1303
|
+
throw new KentuckySignerError("2FA verification cancelled", "2FA_CANCELLED", "User cancelled 2FA input");
|
|
1304
|
+
}
|
|
1305
|
+
const response = await client.signEvmTransactionWith2FA(
|
|
1306
|
+
{ tx_hash: hash, chain_id: chainId, totp_code: codes.totpCode, pin: codes.pin },
|
|
1307
|
+
token
|
|
1308
|
+
);
|
|
1309
|
+
return {
|
|
1310
|
+
r: response.signature.r,
|
|
1311
|
+
s: response.signature.s,
|
|
1312
|
+
v: response.signature.v
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
throw err;
|
|
1316
|
+
}
|
|
419
1317
|
}
|
|
420
1318
|
const account = toAccount({
|
|
421
1319
|
address: session.evmAddress,
|
|
@@ -438,13 +1336,12 @@ function createKentuckySignerAccount(options) {
|
|
|
438
1336
|
const chainId = transaction.chainId ?? defaultChainId;
|
|
439
1337
|
const serializedUnsigned = serializeTransaction(transaction);
|
|
440
1338
|
const txHash = keccak256(serializedUnsigned);
|
|
441
|
-
const
|
|
442
|
-
const { r, s, v } = parseSignature(signature);
|
|
1339
|
+
const { r, s, v } = await signHashWithComponents(txHash, chainId);
|
|
443
1340
|
let yParity;
|
|
444
1341
|
if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
|
|
445
|
-
yParity =
|
|
1342
|
+
yParity = v >= 27 ? v - 27 : v;
|
|
446
1343
|
} else {
|
|
447
|
-
yParity =
|
|
1344
|
+
yParity = v;
|
|
448
1345
|
}
|
|
449
1346
|
const serializedSigned = serializeTransaction(transaction, {
|
|
450
1347
|
r,
|
|
@@ -483,29 +1380,91 @@ function KentuckySignerProvider({
|
|
|
483
1380
|
defaultChainId = 1,
|
|
484
1381
|
storageKeyPrefix = "kentucky_signer",
|
|
485
1382
|
persistSession = true,
|
|
1383
|
+
useEphemeralKeys = false,
|
|
486
1384
|
children
|
|
487
1385
|
}) {
|
|
1386
|
+
const getInitialPersistSetting = () => {
|
|
1387
|
+
if (typeof localStorage !== "undefined") {
|
|
1388
|
+
const saved = localStorage.getItem(`${storageKeyPrefix}_persist_ephemeral_keys`);
|
|
1389
|
+
if (saved !== null) {
|
|
1390
|
+
return saved === "true";
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return typeof indexedDB !== "undefined";
|
|
1394
|
+
};
|
|
1395
|
+
const getInitialSecureModeSetting = () => {
|
|
1396
|
+
if (typeof localStorage !== "undefined") {
|
|
1397
|
+
const saved = localStorage.getItem(`${storageKeyPrefix}_secure_mode`);
|
|
1398
|
+
if (saved !== null) {
|
|
1399
|
+
return saved === "true";
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
return useEphemeralKeys;
|
|
1403
|
+
};
|
|
1404
|
+
const defaultTwoFactorPrompt = {
|
|
1405
|
+
isVisible: false,
|
|
1406
|
+
totpRequired: false,
|
|
1407
|
+
pinRequired: false,
|
|
1408
|
+
pinLength: 6
|
|
1409
|
+
};
|
|
488
1410
|
const [state, setState] = useState({
|
|
489
1411
|
isAuthenticating: false,
|
|
490
1412
|
isAuthenticated: false,
|
|
491
1413
|
session: null,
|
|
492
1414
|
account: null,
|
|
493
|
-
error: null
|
|
1415
|
+
error: null,
|
|
1416
|
+
ephemeralKeyBound: false,
|
|
1417
|
+
secureMode: getInitialSecureModeSetting(),
|
|
1418
|
+
persistEphemeralKeys: getInitialPersistSetting(),
|
|
1419
|
+
twoFactorPrompt: defaultTwoFactorPrompt
|
|
494
1420
|
});
|
|
1421
|
+
const indexedDBStorage = useMemo(
|
|
1422
|
+
() => typeof indexedDB !== "undefined" ? new IndexedDBEphemeralKeyStorage() : null,
|
|
1423
|
+
[]
|
|
1424
|
+
);
|
|
1425
|
+
const memoryStorage = useMemo(() => new MemoryEphemeralKeyStorage(), []);
|
|
1426
|
+
const ephemeralKeyStorage = state.persistEphemeralKeys && indexedDBStorage ? indexedDBStorage : memoryStorage;
|
|
1427
|
+
const ephemeralKeyManagerRef = useRef(null);
|
|
1428
|
+
if (!ephemeralKeyManagerRef.current) {
|
|
1429
|
+
ephemeralKeyManagerRef.current = new EphemeralKeyManager(ephemeralKeyStorage);
|
|
1430
|
+
}
|
|
1431
|
+
const ephemeralKeyManager = ephemeralKeyManagerRef.current;
|
|
495
1432
|
const client = useMemo(
|
|
496
1433
|
() => new KentuckySignerClient({ baseUrl }),
|
|
497
1434
|
[baseUrl]
|
|
498
1435
|
);
|
|
1436
|
+
const secureClient = useMemo(
|
|
1437
|
+
() => new SecureKentuckySignerClient({
|
|
1438
|
+
baseUrl,
|
|
1439
|
+
ephemeralKeyManager
|
|
1440
|
+
}),
|
|
1441
|
+
[baseUrl, ephemeralKeyManager]
|
|
1442
|
+
);
|
|
499
1443
|
const storage = useMemo(
|
|
500
1444
|
() => persistSession ? new LocalStorageTokenStorage(storageKeyPrefix) : null,
|
|
501
1445
|
[persistSession, storageKeyPrefix]
|
|
502
1446
|
);
|
|
1447
|
+
const handle2FARequired = useCallback(async (requirements) => {
|
|
1448
|
+
return new Promise((resolve) => {
|
|
1449
|
+
setState((s) => ({
|
|
1450
|
+
...s,
|
|
1451
|
+
twoFactorPrompt: {
|
|
1452
|
+
isVisible: true,
|
|
1453
|
+
totpRequired: requirements.totpRequired,
|
|
1454
|
+
pinRequired: requirements.pinRequired,
|
|
1455
|
+
pinLength: requirements.pinLength,
|
|
1456
|
+
resolve
|
|
1457
|
+
}
|
|
1458
|
+
}));
|
|
1459
|
+
});
|
|
1460
|
+
}, []);
|
|
503
1461
|
const createAccount = useCallback(
|
|
504
|
-
(session) => {
|
|
1462
|
+
(session, useSecureClient = false) => {
|
|
505
1463
|
return createKentuckySignerAccount({
|
|
506
1464
|
config: { baseUrl, accountId: session.accountId },
|
|
507
1465
|
session,
|
|
508
1466
|
defaultChainId,
|
|
1467
|
+
secureClient: useSecureClient ? secureClient : void 0,
|
|
509
1468
|
onSessionExpired: async () => {
|
|
510
1469
|
const newSession = await refreshSessionIfNeeded(session, baseUrl, 0);
|
|
511
1470
|
setState((s) => ({
|
|
@@ -514,10 +1473,11 @@ function KentuckySignerProvider({
|
|
|
514
1473
|
account: s.account ? { ...s.account, session: newSession } : null
|
|
515
1474
|
}));
|
|
516
1475
|
return newSession;
|
|
517
|
-
}
|
|
1476
|
+
},
|
|
1477
|
+
on2FARequired: handle2FARequired
|
|
518
1478
|
});
|
|
519
1479
|
},
|
|
520
|
-
[baseUrl, defaultChainId]
|
|
1480
|
+
[baseUrl, defaultChainId, secureClient, handle2FARequired]
|
|
521
1481
|
);
|
|
522
1482
|
useEffect(() => {
|
|
523
1483
|
async function restoreSession() {
|
|
@@ -529,30 +1489,42 @@ function KentuckySignerProvider({
|
|
|
529
1489
|
if (!isSessionValid(session)) {
|
|
530
1490
|
try {
|
|
531
1491
|
const refreshed = await refreshSessionIfNeeded(session, baseUrl, 0);
|
|
532
|
-
const account2 = createAccount(refreshed);
|
|
533
1492
|
localStorage.setItem(
|
|
534
1493
|
`${storageKeyPrefix}_session`,
|
|
535
1494
|
JSON.stringify(refreshed)
|
|
536
1495
|
);
|
|
537
|
-
setState({
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
1496
|
+
setState((s) => {
|
|
1497
|
+
const account = createAccount(refreshed, s.secureMode);
|
|
1498
|
+
return {
|
|
1499
|
+
isAuthenticating: false,
|
|
1500
|
+
isAuthenticated: true,
|
|
1501
|
+
session: refreshed,
|
|
1502
|
+
account,
|
|
1503
|
+
error: null,
|
|
1504
|
+
ephemeralKeyBound: false,
|
|
1505
|
+
secureMode: s.secureMode,
|
|
1506
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
1507
|
+
twoFactorPrompt: s.twoFactorPrompt
|
|
1508
|
+
};
|
|
543
1509
|
});
|
|
544
1510
|
} catch {
|
|
545
1511
|
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
546
1512
|
}
|
|
547
1513
|
return;
|
|
548
1514
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1515
|
+
setState((s) => {
|
|
1516
|
+
const account = createAccount(session, s.secureMode);
|
|
1517
|
+
return {
|
|
1518
|
+
isAuthenticating: false,
|
|
1519
|
+
isAuthenticated: true,
|
|
1520
|
+
session,
|
|
1521
|
+
account,
|
|
1522
|
+
error: null,
|
|
1523
|
+
ephemeralKeyBound: false,
|
|
1524
|
+
secureMode: s.secureMode,
|
|
1525
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
1526
|
+
twoFactorPrompt: s.twoFactorPrompt
|
|
1527
|
+
};
|
|
556
1528
|
});
|
|
557
1529
|
} catch {
|
|
558
1530
|
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
@@ -564,24 +1536,42 @@ function KentuckySignerProvider({
|
|
|
564
1536
|
async (accountId, options) => {
|
|
565
1537
|
setState((s) => ({ ...s, isAuthenticating: true, error: null }));
|
|
566
1538
|
try {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
1539
|
+
let session;
|
|
1540
|
+
let ephemeralKeyBound = false;
|
|
1541
|
+
if (options?.session) {
|
|
1542
|
+
session = options.session;
|
|
1543
|
+
} else {
|
|
1544
|
+
let ephemeralPublicKey;
|
|
1545
|
+
if (state.secureMode) {
|
|
1546
|
+
ephemeralPublicKey = await ephemeralKeyManager.getPublicKey();
|
|
1547
|
+
}
|
|
1548
|
+
session = await authenticateWithPasskey({
|
|
1549
|
+
baseUrl,
|
|
1550
|
+
accountId,
|
|
1551
|
+
rpId: options?.rpId,
|
|
1552
|
+
ephemeralPublicKey
|
|
1553
|
+
});
|
|
1554
|
+
ephemeralKeyBound = !!ephemeralPublicKey;
|
|
1555
|
+
}
|
|
573
1556
|
if (storage) {
|
|
574
1557
|
localStorage.setItem(
|
|
575
1558
|
`${storageKeyPrefix}_session`,
|
|
576
1559
|
JSON.stringify(session)
|
|
577
1560
|
);
|
|
578
1561
|
}
|
|
579
|
-
setState({
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1562
|
+
setState((s) => {
|
|
1563
|
+
const account = createAccount(session, s.secureMode);
|
|
1564
|
+
return {
|
|
1565
|
+
isAuthenticating: false,
|
|
1566
|
+
isAuthenticated: true,
|
|
1567
|
+
session,
|
|
1568
|
+
account,
|
|
1569
|
+
error: null,
|
|
1570
|
+
ephemeralKeyBound,
|
|
1571
|
+
secureMode: s.secureMode,
|
|
1572
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
1573
|
+
twoFactorPrompt: s.twoFactorPrompt
|
|
1574
|
+
};
|
|
585
1575
|
});
|
|
586
1576
|
} catch (error) {
|
|
587
1577
|
setState((s) => ({
|
|
@@ -592,7 +1582,7 @@ function KentuckySignerProvider({
|
|
|
592
1582
|
throw error;
|
|
593
1583
|
}
|
|
594
1584
|
},
|
|
595
|
-
[baseUrl, createAccount, storage, storageKeyPrefix]
|
|
1585
|
+
[baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
|
|
596
1586
|
);
|
|
597
1587
|
const logout = useCallback(async () => {
|
|
598
1588
|
try {
|
|
@@ -604,31 +1594,40 @@ function KentuckySignerProvider({
|
|
|
604
1594
|
if (storage) {
|
|
605
1595
|
localStorage.removeItem(`${storageKeyPrefix}_session`);
|
|
606
1596
|
}
|
|
607
|
-
|
|
1597
|
+
if (ephemeralKeyManager) {
|
|
1598
|
+
await ephemeralKeyManager.clear();
|
|
1599
|
+
}
|
|
1600
|
+
setState((s) => ({
|
|
608
1601
|
isAuthenticating: false,
|
|
609
1602
|
isAuthenticated: false,
|
|
610
1603
|
session: null,
|
|
611
1604
|
account: null,
|
|
612
|
-
error: null
|
|
613
|
-
|
|
614
|
-
|
|
1605
|
+
error: null,
|
|
1606
|
+
ephemeralKeyBound: false,
|
|
1607
|
+
secureMode: s.secureMode,
|
|
1608
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
1609
|
+
twoFactorPrompt: defaultTwoFactorPrompt
|
|
1610
|
+
}));
|
|
1611
|
+
}, [client, state.session, storage, storageKeyPrefix, ephemeralKeyManager]);
|
|
615
1612
|
const refreshSession = useCallback(async () => {
|
|
616
1613
|
if (!state.session) return;
|
|
617
1614
|
try {
|
|
618
1615
|
const refreshed = await refreshSessionIfNeeded(state.session, baseUrl);
|
|
619
1616
|
if (refreshed !== state.session) {
|
|
620
|
-
const account = createAccount(refreshed);
|
|
621
1617
|
if (storage) {
|
|
622
1618
|
localStorage.setItem(
|
|
623
1619
|
`${storageKeyPrefix}_session`,
|
|
624
1620
|
JSON.stringify(refreshed)
|
|
625
1621
|
);
|
|
626
1622
|
}
|
|
627
|
-
setState((s) =>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
1623
|
+
setState((s) => {
|
|
1624
|
+
const account = createAccount(refreshed, s.secureMode);
|
|
1625
|
+
return {
|
|
1626
|
+
...s,
|
|
1627
|
+
session: refreshed,
|
|
1628
|
+
account
|
|
1629
|
+
};
|
|
1630
|
+
});
|
|
632
1631
|
}
|
|
633
1632
|
} catch (error) {
|
|
634
1633
|
setState((s) => ({ ...s, error }));
|
|
@@ -637,15 +1636,119 @@ function KentuckySignerProvider({
|
|
|
637
1636
|
const clearError = useCallback(() => {
|
|
638
1637
|
setState((s) => ({ ...s, error: null }));
|
|
639
1638
|
}, []);
|
|
1639
|
+
const setSecureMode = useCallback((enabled) => {
|
|
1640
|
+
if (typeof localStorage !== "undefined") {
|
|
1641
|
+
localStorage.setItem(`${storageKeyPrefix}_secure_mode`, String(enabled));
|
|
1642
|
+
}
|
|
1643
|
+
setState((s) => {
|
|
1644
|
+
const newAccount = s.session ? createAccount(s.session, enabled) : null;
|
|
1645
|
+
return {
|
|
1646
|
+
...s,
|
|
1647
|
+
secureMode: enabled,
|
|
1648
|
+
account: newAccount
|
|
1649
|
+
};
|
|
1650
|
+
});
|
|
1651
|
+
}, [createAccount, storageKeyPrefix]);
|
|
1652
|
+
const setPersistEphemeralKeys = useCallback(async (enabled) => {
|
|
1653
|
+
const newStorage = enabled && indexedDBStorage ? indexedDBStorage : memoryStorage;
|
|
1654
|
+
await ephemeralKeyManager.migrateStorage(newStorage);
|
|
1655
|
+
if (typeof localStorage !== "undefined") {
|
|
1656
|
+
localStorage.setItem(`${storageKeyPrefix}_persist_ephemeral_keys`, String(enabled));
|
|
1657
|
+
}
|
|
1658
|
+
setState((s) => ({
|
|
1659
|
+
...s,
|
|
1660
|
+
persistEphemeralKeys: enabled
|
|
1661
|
+
}));
|
|
1662
|
+
}, [ephemeralKeyManager, storageKeyPrefix, indexedDBStorage, memoryStorage]);
|
|
1663
|
+
const getEphemeralPublicKey = useCallback(async () => {
|
|
1664
|
+
if (!state.secureMode) {
|
|
1665
|
+
return void 0;
|
|
1666
|
+
}
|
|
1667
|
+
return ephemeralKeyManager.getPublicKey();
|
|
1668
|
+
}, [state.secureMode, ephemeralKeyManager]);
|
|
1669
|
+
const authenticatePassword = useCallback(
|
|
1670
|
+
async (accountId, password) => {
|
|
1671
|
+
setState((s) => ({ ...s, isAuthenticating: true, error: null }));
|
|
1672
|
+
try {
|
|
1673
|
+
let ephemeralPublicKey;
|
|
1674
|
+
console.log("[KentuckySigner] authenticatePassword - secureMode:", state.secureMode);
|
|
1675
|
+
if (state.secureMode) {
|
|
1676
|
+
ephemeralPublicKey = await ephemeralKeyManager.getPublicKey();
|
|
1677
|
+
console.log("[KentuckySigner] Got ephemeral public key for binding:", ephemeralPublicKey?.substring(0, 50) + "...");
|
|
1678
|
+
}
|
|
1679
|
+
const session = await authenticateWithPassword({
|
|
1680
|
+
baseUrl,
|
|
1681
|
+
accountId,
|
|
1682
|
+
password,
|
|
1683
|
+
ephemeralPublicKey
|
|
1684
|
+
});
|
|
1685
|
+
const ephemeralKeyBound = !!ephemeralPublicKey;
|
|
1686
|
+
if (storage) {
|
|
1687
|
+
localStorage.setItem(
|
|
1688
|
+
`${storageKeyPrefix}_session`,
|
|
1689
|
+
JSON.stringify(session)
|
|
1690
|
+
);
|
|
1691
|
+
}
|
|
1692
|
+
setState((s) => {
|
|
1693
|
+
const account = createAccount(session, s.secureMode);
|
|
1694
|
+
return {
|
|
1695
|
+
isAuthenticating: false,
|
|
1696
|
+
isAuthenticated: true,
|
|
1697
|
+
session,
|
|
1698
|
+
account,
|
|
1699
|
+
error: null,
|
|
1700
|
+
ephemeralKeyBound,
|
|
1701
|
+
secureMode: s.secureMode,
|
|
1702
|
+
persistEphemeralKeys: s.persistEphemeralKeys,
|
|
1703
|
+
twoFactorPrompt: s.twoFactorPrompt
|
|
1704
|
+
};
|
|
1705
|
+
});
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
setState((s) => ({
|
|
1708
|
+
...s,
|
|
1709
|
+
isAuthenticating: false,
|
|
1710
|
+
error
|
|
1711
|
+
}));
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
},
|
|
1715
|
+
[baseUrl, createAccount, storage, storageKeyPrefix, state.secureMode, ephemeralKeyManager]
|
|
1716
|
+
);
|
|
1717
|
+
const submit2FA = useCallback((codes) => {
|
|
1718
|
+
const { resolve } = state.twoFactorPrompt;
|
|
1719
|
+
if (resolve) {
|
|
1720
|
+
resolve(codes);
|
|
1721
|
+
}
|
|
1722
|
+
setState((s) => ({
|
|
1723
|
+
...s,
|
|
1724
|
+
twoFactorPrompt: defaultTwoFactorPrompt
|
|
1725
|
+
}));
|
|
1726
|
+
}, [state.twoFactorPrompt]);
|
|
1727
|
+
const cancel2FA = useCallback(() => {
|
|
1728
|
+
const { resolve } = state.twoFactorPrompt;
|
|
1729
|
+
if (resolve) {
|
|
1730
|
+
resolve(null);
|
|
1731
|
+
}
|
|
1732
|
+
setState((s) => ({
|
|
1733
|
+
...s,
|
|
1734
|
+
twoFactorPrompt: defaultTwoFactorPrompt
|
|
1735
|
+
}));
|
|
1736
|
+
}, [state.twoFactorPrompt]);
|
|
640
1737
|
const value = useMemo(
|
|
641
1738
|
() => ({
|
|
642
1739
|
...state,
|
|
643
1740
|
authenticate,
|
|
1741
|
+
authenticatePassword,
|
|
644
1742
|
logout,
|
|
645
1743
|
refreshSession,
|
|
646
|
-
clearError
|
|
1744
|
+
clearError,
|
|
1745
|
+
setSecureMode,
|
|
1746
|
+
setPersistEphemeralKeys,
|
|
1747
|
+
getEphemeralPublicKey,
|
|
1748
|
+
submit2FA,
|
|
1749
|
+
cancel2FA
|
|
647
1750
|
}),
|
|
648
|
-
[state, authenticate, logout, refreshSession, clearError]
|
|
1751
|
+
[state, authenticate, authenticatePassword, logout, refreshSession, clearError, setSecureMode, setPersistEphemeralKeys, getEphemeralPublicKey, submit2FA, cancel2FA]
|
|
649
1752
|
);
|
|
650
1753
|
return /* @__PURE__ */ jsx(KentuckySignerContext.Provider, { value, children });
|
|
651
1754
|
}
|
|
@@ -670,10 +1773,21 @@ function useKentuckySigner() {
|
|
|
670
1773
|
session: context.session,
|
|
671
1774
|
account: context.account,
|
|
672
1775
|
error: context.error,
|
|
1776
|
+
ephemeralKeyBound: context.ephemeralKeyBound,
|
|
673
1777
|
authenticate: context.authenticate,
|
|
1778
|
+
authenticatePassword: context.authenticatePassword,
|
|
674
1779
|
logout: context.logout,
|
|
675
1780
|
refreshSession: context.refreshSession,
|
|
676
|
-
clearError: context.clearError
|
|
1781
|
+
clearError: context.clearError,
|
|
1782
|
+
secureMode: context.secureMode,
|
|
1783
|
+
setSecureMode: context.setSecureMode,
|
|
1784
|
+
persistEphemeralKeys: context.persistEphemeralKeys,
|
|
1785
|
+
setPersistEphemeralKeys: context.setPersistEphemeralKeys,
|
|
1786
|
+
getEphemeralPublicKey: context.getEphemeralPublicKey,
|
|
1787
|
+
// 2FA support
|
|
1788
|
+
twoFactorPrompt: context.twoFactorPrompt,
|
|
1789
|
+
submit2FA: context.submit2FA,
|
|
1790
|
+
cancel2FA: context.cancel2FA
|
|
677
1791
|
};
|
|
678
1792
|
}
|
|
679
1793
|
function useKentuckySignerAccount() {
|