decane-connect-kit 2.7.2 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +90 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +89 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -44,6 +44,7 @@ __export(errors_exports, {
|
|
|
44
44
|
NewDeviceError: () => NewDeviceError,
|
|
45
45
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
46
46
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
47
|
+
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
47
48
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
48
49
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
49
50
|
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
@@ -55,7 +56,7 @@ __export(errors_exports, {
|
|
|
55
56
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
56
57
|
WrongPasswordError: () => WrongPasswordError
|
|
57
58
|
});
|
|
58
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
59
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
59
60
|
var init_errors = __esm({
|
|
60
61
|
"src/social/errors.ts"() {
|
|
61
62
|
"use strict";
|
|
@@ -126,6 +127,11 @@ var init_errors = __esm({
|
|
|
126
127
|
super("SHARE_CORRUPTED", "Stored device share is corrupted");
|
|
127
128
|
}
|
|
128
129
|
};
|
|
130
|
+
ProvisioningTimeoutError = class extends DecaneConnectError {
|
|
131
|
+
constructor(step) {
|
|
132
|
+
super("PROVISIONING_TIMEOUT", `Wallet setup timed out while ${step}. Check your connection and try again.`, true);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
129
135
|
NewDeviceError = class extends DecaneConnectError {
|
|
130
136
|
constructor() {
|
|
131
137
|
super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
|
|
@@ -204,6 +210,7 @@ __export(index_exports, {
|
|
|
204
210
|
NewDeviceError: () => NewDeviceError,
|
|
205
211
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
206
212
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
213
|
+
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
207
214
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
208
215
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
209
216
|
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
@@ -2007,6 +2014,7 @@ var ERROR_MAP = {
|
|
|
2007
2014
|
// generic failure message.
|
|
2008
2015
|
UNAUTHORIZED: ReconnectRequiredError
|
|
2009
2016
|
};
|
|
2017
|
+
var REQUEST_TIMEOUT_MS = 3e4;
|
|
2010
2018
|
var ApiClient = class {
|
|
2011
2019
|
constructor(baseUrl, apiKey, appId) {
|
|
2012
2020
|
this.baseUrl = baseUrl;
|
|
@@ -2015,9 +2023,12 @@ var ApiClient = class {
|
|
|
2015
2023
|
}
|
|
2016
2024
|
async _request(path, init) {
|
|
2017
2025
|
let res;
|
|
2026
|
+
const controller = new AbortController();
|
|
2027
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
2018
2028
|
try {
|
|
2019
2029
|
res = await fetch(`${this.baseUrl}${path}`, {
|
|
2020
2030
|
...init,
|
|
2031
|
+
signal: controller.signal,
|
|
2021
2032
|
headers: {
|
|
2022
2033
|
"Content-Type": "application/json",
|
|
2023
2034
|
"X-API-Key": this.apiKey,
|
|
@@ -2027,6 +2038,8 @@ var ApiClient = class {
|
|
|
2027
2038
|
});
|
|
2028
2039
|
} catch {
|
|
2029
2040
|
throw new NetworkError();
|
|
2041
|
+
} finally {
|
|
2042
|
+
clearTimeout(timer);
|
|
2030
2043
|
}
|
|
2031
2044
|
if (res.status === 404 && init?.allow404) return null;
|
|
2032
2045
|
if (!res.ok) {
|
|
@@ -2329,6 +2342,16 @@ function bytesToBase64(b) {
|
|
|
2329
2342
|
function base64ToBytes(s) {
|
|
2330
2343
|
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
2331
2344
|
}
|
|
2345
|
+
var TEE_TIMEOUT_MS = 3e4;
|
|
2346
|
+
async function teeFetch(url, init) {
|
|
2347
|
+
const controller = new AbortController();
|
|
2348
|
+
const timer = setTimeout(() => controller.abort(), TEE_TIMEOUT_MS);
|
|
2349
|
+
try {
|
|
2350
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
2351
|
+
} finally {
|
|
2352
|
+
clearTimeout(timer);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2332
2355
|
var TeeClient = class {
|
|
2333
2356
|
constructor(teeUrl) {
|
|
2334
2357
|
this.teeUrl = teeUrl;
|
|
@@ -2384,7 +2407,7 @@ var TeeClient = class {
|
|
|
2384
2407
|
*/
|
|
2385
2408
|
async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
|
|
2386
2409
|
await this.ensureAttested();
|
|
2387
|
-
const res = await
|
|
2410
|
+
const res = await teeFetch(`${this.teeUrl}/signer/session`, {
|
|
2388
2411
|
method: "POST",
|
|
2389
2412
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2390
2413
|
body: JSON.stringify({
|
|
@@ -2416,7 +2439,7 @@ var TeeClient = class {
|
|
|
2416
2439
|
*/
|
|
2417
2440
|
async recoverWithShare(jwt, recoveryShare) {
|
|
2418
2441
|
await this.ensureAttested();
|
|
2419
|
-
const res = await
|
|
2442
|
+
const res = await teeFetch(`${this.teeUrl}/signer/recover`, {
|
|
2420
2443
|
method: "POST",
|
|
2421
2444
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2422
2445
|
body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
|
|
@@ -2441,7 +2464,7 @@ var TeeClient = class {
|
|
|
2441
2464
|
* require proof of fresh authenticator possession, not just an active session.
|
|
2442
2465
|
*/
|
|
2443
2466
|
async completeAssertion(jwt) {
|
|
2444
|
-
const res = await
|
|
2467
|
+
const res = await teeFetch(`${this.teeUrl}/signer/assertion-challenge`, {
|
|
2445
2468
|
method: "POST",
|
|
2446
2469
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2447
2470
|
body: JSON.stringify({ rpId: window.location.hostname })
|
|
@@ -2470,7 +2493,7 @@ var TeeClient = class {
|
|
|
2470
2493
|
*/
|
|
2471
2494
|
async enrolAuthShare(jwt, recoveryShare) {
|
|
2472
2495
|
await this.ensureAttested();
|
|
2473
|
-
const res = await
|
|
2496
|
+
const res = await teeFetch(`${this.teeUrl}/signer/auth-share`, {
|
|
2474
2497
|
method: "POST",
|
|
2475
2498
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2476
2499
|
body: JSON.stringify({ share: bytesToBase64(recoveryShare) })
|
|
@@ -2493,7 +2516,7 @@ var TeeClient = class {
|
|
|
2493
2516
|
*/
|
|
2494
2517
|
async provisionDeviceShare(jwt) {
|
|
2495
2518
|
await this.ensureAttested();
|
|
2496
|
-
const res = await
|
|
2519
|
+
const res = await teeFetch(`${this.teeUrl}/signer/provision`, {
|
|
2497
2520
|
method: "POST",
|
|
2498
2521
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
|
|
2499
2522
|
});
|
|
@@ -2511,7 +2534,7 @@ var TeeClient = class {
|
|
|
2511
2534
|
async rotateShares(jwt) {
|
|
2512
2535
|
if (!this.handle) throw new Error("no session");
|
|
2513
2536
|
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2514
|
-
const res = await
|
|
2537
|
+
const res = await teeFetch(`${this.teeUrl}/signer/rotate`, {
|
|
2515
2538
|
method: "POST",
|
|
2516
2539
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2517
2540
|
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
@@ -2528,7 +2551,7 @@ var TeeClient = class {
|
|
|
2528
2551
|
async exportPortableBackup(jwt) {
|
|
2529
2552
|
if (!this.handle) throw new Error("no session");
|
|
2530
2553
|
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2531
|
-
const res = await
|
|
2554
|
+
const res = await teeFetch(`${this.teeUrl}/signer/export-portable`, {
|
|
2532
2555
|
method: "POST",
|
|
2533
2556
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2534
2557
|
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
@@ -2558,7 +2581,7 @@ var TeeClient = class {
|
|
|
2558
2581
|
if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
|
|
2559
2582
|
gate = await this.completeAssertion(jwt);
|
|
2560
2583
|
}
|
|
2561
|
-
const res = await
|
|
2584
|
+
const res = await teeFetch(`${this.teeUrl}/signer/sign`, {
|
|
2562
2585
|
method: "POST",
|
|
2563
2586
|
headers: { "Content-Type": "application/json" },
|
|
2564
2587
|
body: JSON.stringify({
|
|
@@ -3499,6 +3522,44 @@ async function createDecaneConnect(config) {
|
|
|
3499
3522
|
let ethProvider = null;
|
|
3500
3523
|
const persistSession = config.persistSession !== false;
|
|
3501
3524
|
const serverAssistedRecovery = true;
|
|
3525
|
+
const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
|
|
3526
|
+
const selfHealAttempted = /* @__PURE__ */ new Set();
|
|
3527
|
+
async function selfHealEnrolment(userId, jwt, share) {
|
|
3528
|
+
try {
|
|
3529
|
+
if (selfHealAttempted.has(userId)) return;
|
|
3530
|
+
const status = await api.getRecoveryStatus(jwt);
|
|
3531
|
+
if (status.enrolled !== false) return;
|
|
3532
|
+
selfHealAttempted.add(userId);
|
|
3533
|
+
await new TeeClient(teeUrl).enrolAuthShare(jwt, share);
|
|
3534
|
+
} catch (e) {
|
|
3535
|
+
events.emit("enrolment-failed");
|
|
3536
|
+
console.warn(
|
|
3537
|
+
"decane-connect-kit: this wallet is not enrolled for server-assisted recovery and the automatic repair failed. It still works on this device, but a new device will need a passkey backup or a recovery file. The SDK will try again on the next unlock.",
|
|
3538
|
+
e
|
|
3539
|
+
);
|
|
3540
|
+
} finally {
|
|
3541
|
+
zeroize(share);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
const PROVISIONING_BUDGET_MS = 9e4;
|
|
3545
|
+
function withDeadline(promise, ms, step) {
|
|
3546
|
+
let timer;
|
|
3547
|
+
return Promise.race([
|
|
3548
|
+
promise,
|
|
3549
|
+
new Promise((_, reject) => {
|
|
3550
|
+
timer = setTimeout(() => reject(new ProvisioningTimeoutError(step)), Math.max(0, ms));
|
|
3551
|
+
})
|
|
3552
|
+
]).finally(() => clearTimeout(timer));
|
|
3553
|
+
}
|
|
3554
|
+
function withTimeout(promise, ms) {
|
|
3555
|
+
let timer;
|
|
3556
|
+
return Promise.race([
|
|
3557
|
+
promise,
|
|
3558
|
+
new Promise((resolve) => {
|
|
3559
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
3560
|
+
})
|
|
3561
|
+
]).finally(() => clearTimeout(timer));
|
|
3562
|
+
}
|
|
3502
3563
|
const SESSION_KEY = sessionPersistKey(config.apiKey);
|
|
3503
3564
|
function persistLiveSession() {
|
|
3504
3565
|
if (!persistSession) return;
|
|
@@ -3639,6 +3700,8 @@ async function createDecaneConnect(config) {
|
|
|
3639
3700
|
({ wrappingKey, meta } = await pinSetup());
|
|
3640
3701
|
unlockMethod = "pin";
|
|
3641
3702
|
}
|
|
3703
|
+
const provisioningStartedAt = Date.now();
|
|
3704
|
+
const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
|
|
3642
3705
|
const master = generateMaster();
|
|
3643
3706
|
let shareSet;
|
|
3644
3707
|
let evmAddress;
|
|
@@ -3670,7 +3733,7 @@ async function createDecaneConnect(config) {
|
|
|
3670
3733
|
version: 1
|
|
3671
3734
|
});
|
|
3672
3735
|
const serverIv = generateRandomBytes(12);
|
|
3673
|
-
await api.putShare(jwt, {
|
|
3736
|
+
await withDeadline(api.putShare(jwt, {
|
|
3674
3737
|
ciphertext: shareSet.server,
|
|
3675
3738
|
iv: serverIv,
|
|
3676
3739
|
wrappedShareMeta: {
|
|
@@ -3680,7 +3743,7 @@ async function createDecaneConnect(config) {
|
|
|
3680
3743
|
evmAddress,
|
|
3681
3744
|
solanaAddress,
|
|
3682
3745
|
tronAddress
|
|
3683
|
-
});
|
|
3746
|
+
}), remainingBudget(), "storing your wallet");
|
|
3684
3747
|
zeroize(shareSet.server);
|
|
3685
3748
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
3686
3749
|
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
|
|
@@ -3696,7 +3759,11 @@ async function createDecaneConnect(config) {
|
|
|
3696
3759
|
console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
|
|
3697
3760
|
}
|
|
3698
3761
|
}
|
|
3699
|
-
const confirmed = await
|
|
3762
|
+
const confirmed = await withDeadline(
|
|
3763
|
+
session.start(shareSet.device, jwt, addresses),
|
|
3764
|
+
remainingBudget(),
|
|
3765
|
+
"opening the signing session"
|
|
3766
|
+
);
|
|
3700
3767
|
zeroize(shareSet.device);
|
|
3701
3768
|
if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3702
3769
|
console.warn(
|
|
@@ -3724,8 +3791,15 @@ async function createDecaneConnect(config) {
|
|
|
3724
3791
|
}
|
|
3725
3792
|
if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
|
|
3726
3793
|
try {
|
|
3727
|
-
const choice = await
|
|
3728
|
-
|
|
3794
|
+
const choice = await withTimeout(
|
|
3795
|
+
config.onRecoveryShareOffer({ evmAddress, solanaAddress }),
|
|
3796
|
+
RECOVERY_OFFER_TIMEOUT_MS
|
|
3797
|
+
);
|
|
3798
|
+
if (!choice) {
|
|
3799
|
+
console.warn(
|
|
3800
|
+
`decane-connect-kit: onRecoveryShareOffer did not settle within ${RECOVERY_OFFER_TIMEOUT_MS / 6e4} minutes, so wallet creation continued without offering a recovery file. The wallet is fully created and enrolled; the user can generate a recovery share later via rotateShares(). Check that your handler always resolves or rejects.`
|
|
3801
|
+
);
|
|
3802
|
+
} else if (choice.wants) {
|
|
3729
3803
|
const file = await buildRecoveryShareFile(
|
|
3730
3804
|
shareSet.recovery,
|
|
3731
3805
|
choice.password,
|
|
@@ -3828,6 +3902,7 @@ async function createDecaneConnect(config) {
|
|
|
3828
3902
|
}
|
|
3829
3903
|
throw e;
|
|
3830
3904
|
}
|
|
3905
|
+
void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
|
|
3831
3906
|
zeroize(deviceShare);
|
|
3832
3907
|
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3833
3908
|
console.warn(
|
|
@@ -7273,6 +7348,7 @@ function useSignTypedData() {
|
|
|
7273
7348
|
NewDeviceError,
|
|
7274
7349
|
OriginNotAllowedError,
|
|
7275
7350
|
PromptPinNotConfiguredError,
|
|
7351
|
+
ProvisioningTimeoutError,
|
|
7276
7352
|
ReconnectRequiredError,
|
|
7277
7353
|
RecoveryFailedError,
|
|
7278
7354
|
RecoveryWalletMismatchError,
|