kentucky-signer-viem 0.1.0
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 +428 -0
- package/dist/index.d.mts +603 -0
- package/dist/index.d.ts +603 -0
- package/dist/index.js +760 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +714 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.d.mts +288 -0
- package/dist/react/index.d.ts +288 -0
- package/dist/react/index.js +815 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +791 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +60 -0
- package/src/account.ts +255 -0
- package/src/auth.ts +427 -0
- package/src/client.ts +308 -0
- package/src/index.ts +73 -0
- package/src/react/context.tsx +317 -0
- package/src/react/hooks.ts +287 -0
- package/src/react/index.ts +31 -0
- package/src/types.ts +237 -0
- package/src/utils.ts +197 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
// src/account.ts
|
|
2
|
+
import {
|
|
3
|
+
hashMessage,
|
|
4
|
+
hashTypedData,
|
|
5
|
+
keccak256,
|
|
6
|
+
serializeTransaction
|
|
7
|
+
} from "viem";
|
|
8
|
+
import { toAccount } from "viem/accounts";
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
var KentuckySignerError = class extends Error {
|
|
12
|
+
constructor(message, code, details) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
this.name = "KentuckySignerError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var KentuckySignerClient = class {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
22
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
23
|
+
this.timeout = options.timeout ?? 3e4;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Make an authenticated request to the API
|
|
27
|
+
*/
|
|
28
|
+
async request(path, options = {}) {
|
|
29
|
+
const { token, ...fetchOptions } = options;
|
|
30
|
+
const headers = {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
...options.headers
|
|
33
|
+
};
|
|
34
|
+
if (token) {
|
|
35
|
+
;
|
|
36
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
37
|
+
}
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
40
|
+
try {
|
|
41
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
42
|
+
...fetchOptions,
|
|
43
|
+
headers,
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
});
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
if (!response.ok || data.success === false) {
|
|
48
|
+
const error = data;
|
|
49
|
+
throw new KentuckySignerError(
|
|
50
|
+
error.error?.message ?? "Unknown error",
|
|
51
|
+
error.error?.code ?? "UNKNOWN_ERROR",
|
|
52
|
+
error.error?.details
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return data;
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get a challenge for passkey authentication
|
|
62
|
+
*
|
|
63
|
+
* @param accountId - Account ID to authenticate
|
|
64
|
+
* @returns Challenge response with 32-byte challenge
|
|
65
|
+
*/
|
|
66
|
+
async getChallenge(accountId) {
|
|
67
|
+
return this.request("/api/auth/challenge", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: JSON.stringify({ account_id: accountId })
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Authenticate with a passkey credential
|
|
74
|
+
*
|
|
75
|
+
* @param accountId - Account ID to authenticate
|
|
76
|
+
* @param credential - WebAuthn credential from navigator.credentials.get()
|
|
77
|
+
* @returns Authentication response with JWT token
|
|
78
|
+
*/
|
|
79
|
+
async authenticatePasskey(accountId, credential) {
|
|
80
|
+
return this.request("/api/auth/passkey", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({
|
|
83
|
+
account_id: accountId,
|
|
84
|
+
credential_id: credential.credentialId,
|
|
85
|
+
client_data_json: credential.clientDataJSON,
|
|
86
|
+
authenticator_data: credential.authenticatorData,
|
|
87
|
+
signature: credential.signature,
|
|
88
|
+
user_handle: credential.userHandle
|
|
89
|
+
})
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Refresh an authentication token
|
|
94
|
+
*
|
|
95
|
+
* @param token - Current JWT token
|
|
96
|
+
* @returns New authentication response with fresh token
|
|
97
|
+
*/
|
|
98
|
+
async refreshToken(token) {
|
|
99
|
+
return this.request("/api/auth/refresh", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
token
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Logout and invalidate token
|
|
106
|
+
*
|
|
107
|
+
* @param token - JWT token to invalidate
|
|
108
|
+
*/
|
|
109
|
+
async logout(token) {
|
|
110
|
+
await this.request("/api/auth/logout", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
token
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get account information
|
|
117
|
+
*
|
|
118
|
+
* @param accountId - Account ID
|
|
119
|
+
* @param token - JWT token
|
|
120
|
+
* @returns Account info with addresses and passkeys
|
|
121
|
+
*/
|
|
122
|
+
async getAccountInfo(accountId, token) {
|
|
123
|
+
return this.request(`/api/accounts/${accountId}`, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
token
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Check if an account exists
|
|
130
|
+
*
|
|
131
|
+
* @param accountId - Account ID
|
|
132
|
+
* @param token - JWT token
|
|
133
|
+
* @returns True if account exists
|
|
134
|
+
*/
|
|
135
|
+
async accountExists(accountId, token) {
|
|
136
|
+
try {
|
|
137
|
+
await this.request(`/api/accounts/${accountId}`, {
|
|
138
|
+
method: "HEAD",
|
|
139
|
+
token
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Sign an EVM transaction hash
|
|
148
|
+
*
|
|
149
|
+
* @param request - Sign request with tx_hash and chain_id
|
|
150
|
+
* @param token - JWT token
|
|
151
|
+
* @returns Signature response with r, s, v components
|
|
152
|
+
*/
|
|
153
|
+
async signEvmTransaction(request, token) {
|
|
154
|
+
return this.request("/api/sign/evm", {
|
|
155
|
+
method: "POST",
|
|
156
|
+
token,
|
|
157
|
+
body: JSON.stringify(request)
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Sign a raw hash for EVM
|
|
162
|
+
*
|
|
163
|
+
* Convenience method that wraps signEvmTransaction.
|
|
164
|
+
*
|
|
165
|
+
* @param hash - 32-byte hash to sign (hex encoded with 0x prefix)
|
|
166
|
+
* @param chainId - Chain ID
|
|
167
|
+
* @param token - JWT token
|
|
168
|
+
* @returns Full signature (hex encoded with 0x prefix)
|
|
169
|
+
*/
|
|
170
|
+
async signHash(hash, chainId, token) {
|
|
171
|
+
const response = await this.signEvmTransaction(
|
|
172
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
173
|
+
token
|
|
174
|
+
);
|
|
175
|
+
return response.signature.full;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Create a new account with passkey authentication
|
|
179
|
+
*
|
|
180
|
+
* @param credential - WebAuthn credential from navigator.credentials.create()
|
|
181
|
+
* @returns Account creation response with account ID and addresses
|
|
182
|
+
*/
|
|
183
|
+
async createAccountWithPasskey(credential) {
|
|
184
|
+
return this.request("/api/accounts/create/passkey", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
credential_id: credential.credentialId,
|
|
188
|
+
public_key: credential.publicKey,
|
|
189
|
+
client_data_json: credential.clientDataJSON,
|
|
190
|
+
authenticator_data: credential.authenticatorData
|
|
191
|
+
})
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Create a new account with password authentication
|
|
196
|
+
*
|
|
197
|
+
* @param request - Password and confirmation
|
|
198
|
+
* @returns Account creation response with account ID and addresses
|
|
199
|
+
*/
|
|
200
|
+
async createAccountWithPassword(request) {
|
|
201
|
+
return this.request("/api/accounts/create/password", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
body: JSON.stringify(request)
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Authenticate with password
|
|
208
|
+
*
|
|
209
|
+
* @param request - Account ID and password
|
|
210
|
+
* @returns Authentication response with JWT token
|
|
211
|
+
*/
|
|
212
|
+
async authenticatePassword(request) {
|
|
213
|
+
return this.request("/api/auth/password", {
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: JSON.stringify(request)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Health check
|
|
220
|
+
*
|
|
221
|
+
* @returns True if the API is healthy
|
|
222
|
+
*/
|
|
223
|
+
async healthCheck() {
|
|
224
|
+
try {
|
|
225
|
+
const response = await this.request("/api/health", {
|
|
226
|
+
method: "GET"
|
|
227
|
+
});
|
|
228
|
+
return response.status === "ok";
|
|
229
|
+
} catch {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get API version
|
|
235
|
+
*
|
|
236
|
+
* @returns Version string
|
|
237
|
+
*/
|
|
238
|
+
async getVersion() {
|
|
239
|
+
const response = await this.request("/api/version", {
|
|
240
|
+
method: "GET"
|
|
241
|
+
});
|
|
242
|
+
return response.version;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
function createClient(options) {
|
|
246
|
+
return new KentuckySignerClient(options);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/account.ts
|
|
250
|
+
function createKentuckySignerAccount(options) {
|
|
251
|
+
const { config, defaultChainId = 1, onSessionExpired } = options;
|
|
252
|
+
let session = options.session;
|
|
253
|
+
const client = new KentuckySignerClient({ baseUrl: config.baseUrl });
|
|
254
|
+
async function getToken() {
|
|
255
|
+
if (Date.now() + 6e4 >= session.expiresAt) {
|
|
256
|
+
if (onSessionExpired) {
|
|
257
|
+
session = await onSessionExpired();
|
|
258
|
+
} else {
|
|
259
|
+
throw new KentuckySignerError(
|
|
260
|
+
"Session expired",
|
|
261
|
+
"SESSION_EXPIRED",
|
|
262
|
+
"Please re-authenticate with your passkey"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return session.token;
|
|
267
|
+
}
|
|
268
|
+
async function signHash(hash, chainId) {
|
|
269
|
+
const token = await getToken();
|
|
270
|
+
const response = await client.signEvmTransaction(
|
|
271
|
+
{ tx_hash: hash, chain_id: chainId },
|
|
272
|
+
token
|
|
273
|
+
);
|
|
274
|
+
return response.signature.full;
|
|
275
|
+
}
|
|
276
|
+
function parseSignature(signature) {
|
|
277
|
+
const r = `0x${signature.slice(2, 66)}`;
|
|
278
|
+
const s = `0x${signature.slice(66, 130)}`;
|
|
279
|
+
const v = BigInt(`0x${signature.slice(130, 132)}`);
|
|
280
|
+
return { r, s, v };
|
|
281
|
+
}
|
|
282
|
+
const account = toAccount({
|
|
283
|
+
address: session.evmAddress,
|
|
284
|
+
/**
|
|
285
|
+
* Sign a message
|
|
286
|
+
*
|
|
287
|
+
* Supports string messages, hex messages, and raw bytes.
|
|
288
|
+
*/
|
|
289
|
+
async signMessage({ message }) {
|
|
290
|
+
const messageHash = hashMessage(message);
|
|
291
|
+
return signHash(messageHash, defaultChainId);
|
|
292
|
+
},
|
|
293
|
+
/**
|
|
294
|
+
* Sign a transaction
|
|
295
|
+
*
|
|
296
|
+
* Serializes the transaction, hashes it, signs via Kentucky Signer,
|
|
297
|
+
* and returns the signed serialized transaction.
|
|
298
|
+
*/
|
|
299
|
+
async signTransaction(transaction) {
|
|
300
|
+
const chainId = transaction.chainId ?? defaultChainId;
|
|
301
|
+
const serializedUnsigned = serializeTransaction(transaction);
|
|
302
|
+
const txHash = keccak256(serializedUnsigned);
|
|
303
|
+
const signature = await signHash(txHash, chainId);
|
|
304
|
+
const { r, s, v } = parseSignature(signature);
|
|
305
|
+
let yParity;
|
|
306
|
+
if (transaction.type === "eip1559" || transaction.type === "eip2930" || transaction.type === "eip4844" || transaction.type === "eip7702") {
|
|
307
|
+
yParity = Number(v) - 27;
|
|
308
|
+
} else {
|
|
309
|
+
yParity = Number(v);
|
|
310
|
+
}
|
|
311
|
+
const serializedSigned = serializeTransaction(transaction, {
|
|
312
|
+
r,
|
|
313
|
+
s,
|
|
314
|
+
v: BigInt(yParity),
|
|
315
|
+
yParity
|
|
316
|
+
});
|
|
317
|
+
return serializedSigned;
|
|
318
|
+
},
|
|
319
|
+
/**
|
|
320
|
+
* Sign typed data (EIP-712)
|
|
321
|
+
*/
|
|
322
|
+
async signTypedData(typedData) {
|
|
323
|
+
const hash = hashTypedData(typedData);
|
|
324
|
+
return signHash(hash, defaultChainId);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
account.source = "kentuckySigner";
|
|
328
|
+
account.accountId = config.accountId;
|
|
329
|
+
account.session = session;
|
|
330
|
+
account.updateSession = (newSession) => {
|
|
331
|
+
session = newSession;
|
|
332
|
+
if (newSession.evmAddress !== account.address) {
|
|
333
|
+
;
|
|
334
|
+
account.address = newSession.evmAddress;
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
return account;
|
|
338
|
+
}
|
|
339
|
+
function createServerAccount(baseUrl, accountId, token, evmAddress, chainId = 1) {
|
|
340
|
+
const session = {
|
|
341
|
+
token,
|
|
342
|
+
accountId,
|
|
343
|
+
evmAddress,
|
|
344
|
+
expiresAt: Date.now() + 36e5
|
|
345
|
+
// 1 hour default
|
|
346
|
+
};
|
|
347
|
+
return createKentuckySignerAccount({
|
|
348
|
+
config: { baseUrl, accountId },
|
|
349
|
+
session,
|
|
350
|
+
defaultChainId: chainId
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/utils.ts
|
|
355
|
+
function base64UrlEncode(data) {
|
|
356
|
+
let base64;
|
|
357
|
+
if (typeof Buffer !== "undefined") {
|
|
358
|
+
base64 = Buffer.from(data).toString("base64");
|
|
359
|
+
} else {
|
|
360
|
+
const binary = Array.from(data).map((byte) => String.fromCharCode(byte)).join("");
|
|
361
|
+
base64 = btoa(binary);
|
|
362
|
+
}
|
|
363
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
364
|
+
}
|
|
365
|
+
function base64UrlDecode(str) {
|
|
366
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
367
|
+
const padding = (4 - base64.length % 4) % 4;
|
|
368
|
+
base64 += "=".repeat(padding);
|
|
369
|
+
if (typeof Buffer !== "undefined") {
|
|
370
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
371
|
+
} else {
|
|
372
|
+
const binary = atob(base64);
|
|
373
|
+
const bytes = new Uint8Array(binary.length);
|
|
374
|
+
for (let i = 0; i < binary.length; i++) {
|
|
375
|
+
bytes[i] = binary.charCodeAt(i);
|
|
376
|
+
}
|
|
377
|
+
return bytes;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function hexToBytes(hex) {
|
|
381
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
382
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
383
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
384
|
+
bytes[i] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16);
|
|
385
|
+
}
|
|
386
|
+
return bytes;
|
|
387
|
+
}
|
|
388
|
+
function bytesToHex(bytes, withPrefix = true) {
|
|
389
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
390
|
+
return withPrefix ? `0x${hex}` : hex;
|
|
391
|
+
}
|
|
392
|
+
function isValidAccountId(accountId) {
|
|
393
|
+
return /^[0-9a-fA-F]{64}$/.test(accountId);
|
|
394
|
+
}
|
|
395
|
+
function isValidEvmAddress(address) {
|
|
396
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
397
|
+
}
|
|
398
|
+
function sleep(ms) {
|
|
399
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
400
|
+
}
|
|
401
|
+
async function withRetry(fn, maxRetries = 3, baseDelay = 1e3) {
|
|
402
|
+
let lastError;
|
|
403
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
404
|
+
try {
|
|
405
|
+
return await fn();
|
|
406
|
+
} catch (error) {
|
|
407
|
+
lastError = error;
|
|
408
|
+
if (attempt < maxRetries) {
|
|
409
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
410
|
+
await sleep(delay);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw lastError;
|
|
415
|
+
}
|
|
416
|
+
function parseJwt(token) {
|
|
417
|
+
const parts = token.split(".");
|
|
418
|
+
if (parts.length !== 3) {
|
|
419
|
+
throw new Error("Invalid JWT format");
|
|
420
|
+
}
|
|
421
|
+
const payload = base64UrlDecode(parts[1]);
|
|
422
|
+
const text = new TextDecoder().decode(payload);
|
|
423
|
+
return JSON.parse(text);
|
|
424
|
+
}
|
|
425
|
+
function getJwtExpiration(token) {
|
|
426
|
+
try {
|
|
427
|
+
const payload = parseJwt(token);
|
|
428
|
+
if (typeof payload.exp === "number") {
|
|
429
|
+
return payload.exp * 1e3;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
} catch {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function formatError(error) {
|
|
437
|
+
if (error instanceof Error) {
|
|
438
|
+
return error.message;
|
|
439
|
+
}
|
|
440
|
+
if (typeof error === "string") {
|
|
441
|
+
return error;
|
|
442
|
+
}
|
|
443
|
+
return "An unknown error occurred";
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/auth.ts
|
|
447
|
+
function isWebAuthnAvailable() {
|
|
448
|
+
return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof navigator.credentials !== "undefined";
|
|
449
|
+
}
|
|
450
|
+
var MemoryTokenStorage = class {
|
|
451
|
+
constructor() {
|
|
452
|
+
this.token = null;
|
|
453
|
+
this.expiresAt = 0;
|
|
454
|
+
}
|
|
455
|
+
async getToken() {
|
|
456
|
+
if (this.token && Date.now() < this.expiresAt) {
|
|
457
|
+
return this.token;
|
|
458
|
+
}
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
async setToken(token, expiresAt) {
|
|
462
|
+
this.token = token;
|
|
463
|
+
this.expiresAt = expiresAt;
|
|
464
|
+
}
|
|
465
|
+
async clearToken() {
|
|
466
|
+
this.token = null;
|
|
467
|
+
this.expiresAt = 0;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
var LocalStorageTokenStorage = class {
|
|
471
|
+
constructor(keyPrefix = "kentucky_signer") {
|
|
472
|
+
this.keyPrefix = keyPrefix;
|
|
473
|
+
}
|
|
474
|
+
async getToken() {
|
|
475
|
+
if (typeof localStorage === "undefined") return null;
|
|
476
|
+
const token = localStorage.getItem(`${this.keyPrefix}_token`);
|
|
477
|
+
const expiresAt = localStorage.getItem(`${this.keyPrefix}_expires`);
|
|
478
|
+
if (token && expiresAt && Date.now() < parseInt(expiresAt, 10)) {
|
|
479
|
+
return token;
|
|
480
|
+
}
|
|
481
|
+
await this.clearToken();
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
async setToken(token, expiresAt) {
|
|
485
|
+
if (typeof localStorage === "undefined") return;
|
|
486
|
+
localStorage.setItem(`${this.keyPrefix}_token`, token);
|
|
487
|
+
localStorage.setItem(`${this.keyPrefix}_expires`, expiresAt.toString());
|
|
488
|
+
}
|
|
489
|
+
async clearToken() {
|
|
490
|
+
if (typeof localStorage === "undefined") return;
|
|
491
|
+
localStorage.removeItem(`${this.keyPrefix}_token`);
|
|
492
|
+
localStorage.removeItem(`${this.keyPrefix}_expires`);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
function credentialToPasskey(credential) {
|
|
496
|
+
const response = credential.response;
|
|
497
|
+
return {
|
|
498
|
+
credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
|
|
499
|
+
clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
|
|
500
|
+
authenticatorData: base64UrlEncode(new Uint8Array(response.authenticatorData)),
|
|
501
|
+
signature: base64UrlEncode(new Uint8Array(response.signature)),
|
|
502
|
+
userHandle: response.userHandle ? base64UrlEncode(new Uint8Array(response.userHandle)) : void 0
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async function authenticateWithPasskey(options) {
|
|
506
|
+
if (!isWebAuthnAvailable()) {
|
|
507
|
+
throw new KentuckySignerError(
|
|
508
|
+
"WebAuthn is not available in this environment",
|
|
509
|
+
"WEBAUTHN_NOT_AVAILABLE",
|
|
510
|
+
"Use authenticateWithToken() for server-side authentication"
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
|
|
514
|
+
const challengeResponse = await client.getChallenge(options.accountId);
|
|
515
|
+
const challengeBytes = base64UrlDecode(challengeResponse.challenge);
|
|
516
|
+
const credentialRequestOptions = {
|
|
517
|
+
publicKey: {
|
|
518
|
+
challenge: challengeBytes.buffer,
|
|
519
|
+
timeout: 6e4,
|
|
520
|
+
rpId: options.rpId ?? window.location.hostname,
|
|
521
|
+
userVerification: "preferred",
|
|
522
|
+
allowCredentials: options.allowCredentials?.map((id) => ({
|
|
523
|
+
type: "public-key",
|
|
524
|
+
id: base64UrlDecode(id).buffer
|
|
525
|
+
}))
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
const credential = await navigator.credentials.get(
|
|
529
|
+
credentialRequestOptions
|
|
530
|
+
);
|
|
531
|
+
if (!credential) {
|
|
532
|
+
throw new KentuckySignerError(
|
|
533
|
+
"User cancelled passkey authentication",
|
|
534
|
+
"USER_CANCELLED"
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const passkeyCredential = credentialToPasskey(credential);
|
|
538
|
+
const authResponse = await client.authenticatePasskey(
|
|
539
|
+
options.accountId,
|
|
540
|
+
passkeyCredential
|
|
541
|
+
);
|
|
542
|
+
const accountInfo = await client.getAccountInfo(
|
|
543
|
+
options.accountId,
|
|
544
|
+
authResponse.token
|
|
545
|
+
);
|
|
546
|
+
const expiresAt = Date.now() + authResponse.expires_in * 1e3;
|
|
547
|
+
return {
|
|
548
|
+
token: authResponse.token,
|
|
549
|
+
accountId: options.accountId,
|
|
550
|
+
evmAddress: accountInfo.addresses.evm,
|
|
551
|
+
btcAddress: accountInfo.addresses.bitcoin,
|
|
552
|
+
solAddress: accountInfo.addresses.solana,
|
|
553
|
+
expiresAt
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
async function authenticateWithToken(baseUrl, accountId, token, expiresAt) {
|
|
557
|
+
const client = new KentuckySignerClient({ baseUrl });
|
|
558
|
+
const accountInfo = await client.getAccountInfo(accountId, token);
|
|
559
|
+
return {
|
|
560
|
+
token,
|
|
561
|
+
accountId,
|
|
562
|
+
evmAddress: accountInfo.addresses.evm,
|
|
563
|
+
btcAddress: accountInfo.addresses.bitcoin,
|
|
564
|
+
solAddress: accountInfo.addresses.solana,
|
|
565
|
+
expiresAt: expiresAt ?? Date.now() + 36e5
|
|
566
|
+
// Default 1 hour if not specified
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async function registerPasskey(options) {
|
|
570
|
+
if (!isWebAuthnAvailable()) {
|
|
571
|
+
throw new KentuckySignerError(
|
|
572
|
+
"WebAuthn is not available in this environment",
|
|
573
|
+
"WEBAUTHN_NOT_AVAILABLE"
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const userId = new Uint8Array(32);
|
|
577
|
+
crypto.getRandomValues(userId);
|
|
578
|
+
const challenge = new Uint8Array(32);
|
|
579
|
+
crypto.getRandomValues(challenge);
|
|
580
|
+
const createOptions = {
|
|
581
|
+
publicKey: {
|
|
582
|
+
challenge,
|
|
583
|
+
rp: {
|
|
584
|
+
name: options.rpName ?? "Kentucky Signer",
|
|
585
|
+
id: options.rpId ?? window.location.hostname
|
|
586
|
+
},
|
|
587
|
+
user: {
|
|
588
|
+
id: userId,
|
|
589
|
+
name: options.username ?? "user@example.com",
|
|
590
|
+
displayName: options.username ?? "User"
|
|
591
|
+
},
|
|
592
|
+
pubKeyCredParams: [
|
|
593
|
+
{ type: "public-key", alg: -7 },
|
|
594
|
+
// ES256 (P-256)
|
|
595
|
+
{ type: "public-key", alg: -257 }
|
|
596
|
+
// RS256
|
|
597
|
+
],
|
|
598
|
+
authenticatorSelection: {
|
|
599
|
+
authenticatorAttachment: "platform",
|
|
600
|
+
userVerification: "preferred",
|
|
601
|
+
residentKey: "preferred"
|
|
602
|
+
},
|
|
603
|
+
timeout: 6e4,
|
|
604
|
+
attestation: "none"
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
const credential = await navigator.credentials.create(
|
|
608
|
+
createOptions
|
|
609
|
+
);
|
|
610
|
+
if (!credential) {
|
|
611
|
+
throw new KentuckySignerError(
|
|
612
|
+
"User cancelled passkey registration",
|
|
613
|
+
"USER_CANCELLED"
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
const response = credential.response;
|
|
617
|
+
const publicKeyBytes = response.getPublicKey?.();
|
|
618
|
+
if (!publicKeyBytes) {
|
|
619
|
+
throw new KentuckySignerError(
|
|
620
|
+
"Failed to get public key from credential",
|
|
621
|
+
"PUBLIC_KEY_ERROR"
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
credentialId: base64UrlEncode(new Uint8Array(credential.rawId)),
|
|
626
|
+
clientDataJSON: base64UrlEncode(new Uint8Array(response.clientDataJSON)),
|
|
627
|
+
authenticatorData: base64UrlEncode(new Uint8Array(response.getAuthenticatorData())),
|
|
628
|
+
signature: "",
|
|
629
|
+
// Not applicable for registration
|
|
630
|
+
publicKey: base64UrlEncode(new Uint8Array(publicKeyBytes))
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function isSessionValid(session, bufferMs = 6e4) {
|
|
634
|
+
return Date.now() + bufferMs < session.expiresAt;
|
|
635
|
+
}
|
|
636
|
+
async function refreshSessionIfNeeded(session, baseUrl, bufferMs = 6e4) {
|
|
637
|
+
if (isSessionValid(session, bufferMs)) {
|
|
638
|
+
return session;
|
|
639
|
+
}
|
|
640
|
+
const client = new KentuckySignerClient({ baseUrl });
|
|
641
|
+
const authResponse = await client.refreshToken(session.token);
|
|
642
|
+
return {
|
|
643
|
+
...session,
|
|
644
|
+
token: authResponse.token,
|
|
645
|
+
expiresAt: Date.now() + authResponse.expires_in * 1e3
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
async function authenticateWithPassword(options) {
|
|
649
|
+
const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
|
|
650
|
+
const authResponse = await client.authenticatePassword({
|
|
651
|
+
account_id: options.accountId,
|
|
652
|
+
password: options.password
|
|
653
|
+
});
|
|
654
|
+
const accountInfo = await client.getAccountInfo(
|
|
655
|
+
options.accountId,
|
|
656
|
+
authResponse.token
|
|
657
|
+
);
|
|
658
|
+
const expiresAt = Date.now() + authResponse.expires_in * 1e3;
|
|
659
|
+
return {
|
|
660
|
+
token: authResponse.token,
|
|
661
|
+
accountId: options.accountId,
|
|
662
|
+
evmAddress: accountInfo.addresses.evm,
|
|
663
|
+
btcAddress: accountInfo.addresses.bitcoin,
|
|
664
|
+
solAddress: accountInfo.addresses.solana,
|
|
665
|
+
expiresAt
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async function createAccountWithPassword(options) {
|
|
669
|
+
if (options.password !== options.confirmation) {
|
|
670
|
+
throw new KentuckySignerError(
|
|
671
|
+
"Password and confirmation do not match",
|
|
672
|
+
"PASSWORD_MISMATCH"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
if (options.password.length < 8 || options.password.length > 128) {
|
|
676
|
+
throw new KentuckySignerError(
|
|
677
|
+
"Password must be 8-128 characters",
|
|
678
|
+
"INVALID_PASSWORD"
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
const client = new KentuckySignerClient({ baseUrl: options.baseUrl });
|
|
682
|
+
return client.createAccountWithPassword({
|
|
683
|
+
password: options.password,
|
|
684
|
+
confirmation: options.confirmation
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
export {
|
|
688
|
+
KentuckySignerClient,
|
|
689
|
+
KentuckySignerError,
|
|
690
|
+
LocalStorageTokenStorage,
|
|
691
|
+
MemoryTokenStorage,
|
|
692
|
+
authenticateWithPasskey,
|
|
693
|
+
authenticateWithPassword,
|
|
694
|
+
authenticateWithToken,
|
|
695
|
+
base64UrlDecode,
|
|
696
|
+
base64UrlEncode,
|
|
697
|
+
bytesToHex,
|
|
698
|
+
createAccountWithPassword,
|
|
699
|
+
createClient,
|
|
700
|
+
createKentuckySignerAccount,
|
|
701
|
+
createServerAccount,
|
|
702
|
+
formatError,
|
|
703
|
+
getJwtExpiration,
|
|
704
|
+
hexToBytes,
|
|
705
|
+
isSessionValid,
|
|
706
|
+
isValidAccountId,
|
|
707
|
+
isValidEvmAddress,
|
|
708
|
+
isWebAuthnAvailable,
|
|
709
|
+
parseJwt,
|
|
710
|
+
refreshSessionIfNeeded,
|
|
711
|
+
registerPasskey,
|
|
712
|
+
withRetry
|
|
713
|
+
};
|
|
714
|
+
//# sourceMappingURL=index.mjs.map
|