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