decane-connect-kit 2.7.1 → 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 +121 -27
- 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 +120 -27
- 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,12 +2493,17 @@ 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) })
|
|
2477
2500
|
});
|
|
2478
|
-
if (!res.ok)
|
|
2501
|
+
if (!res.ok) {
|
|
2502
|
+
const detail = await res.text().catch(() => "");
|
|
2503
|
+
throw new Error(
|
|
2504
|
+
`auth share enrolment failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2479
2507
|
}
|
|
2480
2508
|
/**
|
|
2481
2509
|
* Ask the enclave for a fresh device share for this authenticated user.
|
|
@@ -2488,12 +2516,17 @@ var TeeClient = class {
|
|
|
2488
2516
|
*/
|
|
2489
2517
|
async provisionDeviceShare(jwt) {
|
|
2490
2518
|
await this.ensureAttested();
|
|
2491
|
-
const res = await
|
|
2519
|
+
const res = await teeFetch(`${this.teeUrl}/signer/provision`, {
|
|
2492
2520
|
method: "POST",
|
|
2493
2521
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
|
|
2494
2522
|
});
|
|
2495
2523
|
if (res.status === 409) return null;
|
|
2496
|
-
if (!res.ok)
|
|
2524
|
+
if (!res.ok) {
|
|
2525
|
+
const detail = await res.text().catch(() => "");
|
|
2526
|
+
throw new Error(
|
|
2527
|
+
`device share provisioning failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2497
2530
|
const body = await res.json();
|
|
2498
2531
|
return base64ToBytes(body.deviceShare);
|
|
2499
2532
|
}
|
|
@@ -2501,7 +2534,7 @@ var TeeClient = class {
|
|
|
2501
2534
|
async rotateShares(jwt) {
|
|
2502
2535
|
if (!this.handle) throw new Error("no session");
|
|
2503
2536
|
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2504
|
-
const res = await
|
|
2537
|
+
const res = await teeFetch(`${this.teeUrl}/signer/rotate`, {
|
|
2505
2538
|
method: "POST",
|
|
2506
2539
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2507
2540
|
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
@@ -2518,7 +2551,7 @@ var TeeClient = class {
|
|
|
2518
2551
|
async exportPortableBackup(jwt) {
|
|
2519
2552
|
if (!this.handle) throw new Error("no session");
|
|
2520
2553
|
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2521
|
-
const res = await
|
|
2554
|
+
const res = await teeFetch(`${this.teeUrl}/signer/export-portable`, {
|
|
2522
2555
|
method: "POST",
|
|
2523
2556
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2524
2557
|
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
@@ -2548,7 +2581,7 @@ var TeeClient = class {
|
|
|
2548
2581
|
if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
|
|
2549
2582
|
gate = await this.completeAssertion(jwt);
|
|
2550
2583
|
}
|
|
2551
|
-
const res = await
|
|
2584
|
+
const res = await teeFetch(`${this.teeUrl}/signer/sign`, {
|
|
2552
2585
|
method: "POST",
|
|
2553
2586
|
headers: { "Content-Type": "application/json" },
|
|
2554
2587
|
body: JSON.stringify({
|
|
@@ -3489,6 +3522,44 @@ async function createDecaneConnect(config) {
|
|
|
3489
3522
|
let ethProvider = null;
|
|
3490
3523
|
const persistSession = config.persistSession !== false;
|
|
3491
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
|
+
}
|
|
3492
3563
|
const SESSION_KEY = sessionPersistKey(config.apiKey);
|
|
3493
3564
|
function persistLiveSession() {
|
|
3494
3565
|
if (!persistSession) return;
|
|
@@ -3629,6 +3700,8 @@ async function createDecaneConnect(config) {
|
|
|
3629
3700
|
({ wrappingKey, meta } = await pinSetup());
|
|
3630
3701
|
unlockMethod = "pin";
|
|
3631
3702
|
}
|
|
3703
|
+
const provisioningStartedAt = Date.now();
|
|
3704
|
+
const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
|
|
3632
3705
|
const master = generateMaster();
|
|
3633
3706
|
let shareSet;
|
|
3634
3707
|
let evmAddress;
|
|
@@ -3660,7 +3733,7 @@ async function createDecaneConnect(config) {
|
|
|
3660
3733
|
version: 1
|
|
3661
3734
|
});
|
|
3662
3735
|
const serverIv = generateRandomBytes(12);
|
|
3663
|
-
await api.putShare(jwt, {
|
|
3736
|
+
await withDeadline(api.putShare(jwt, {
|
|
3664
3737
|
ciphertext: shareSet.server,
|
|
3665
3738
|
iv: serverIv,
|
|
3666
3739
|
wrappedShareMeta: {
|
|
@@ -3670,7 +3743,7 @@ async function createDecaneConnect(config) {
|
|
|
3670
3743
|
evmAddress,
|
|
3671
3744
|
solanaAddress,
|
|
3672
3745
|
tronAddress
|
|
3673
|
-
});
|
|
3746
|
+
}), remainingBudget(), "storing your wallet");
|
|
3674
3747
|
zeroize(shareSet.server);
|
|
3675
3748
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
3676
3749
|
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
|
|
@@ -3686,7 +3759,11 @@ async function createDecaneConnect(config) {
|
|
|
3686
3759
|
console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
|
|
3687
3760
|
}
|
|
3688
3761
|
}
|
|
3689
|
-
const confirmed = await
|
|
3762
|
+
const confirmed = await withDeadline(
|
|
3763
|
+
session.start(shareSet.device, jwt, addresses),
|
|
3764
|
+
remainingBudget(),
|
|
3765
|
+
"opening the signing session"
|
|
3766
|
+
);
|
|
3690
3767
|
zeroize(shareSet.device);
|
|
3691
3768
|
if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3692
3769
|
console.warn(
|
|
@@ -3696,10 +3773,33 @@ async function createDecaneConnect(config) {
|
|
|
3696
3773
|
addresses.evm = evmAddress = confirmed.evm;
|
|
3697
3774
|
addresses.solana = solanaAddress = confirmed.solana;
|
|
3698
3775
|
addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
|
|
3776
|
+
if (serverAssistedRecovery) {
|
|
3777
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
3778
|
+
try {
|
|
3779
|
+
const tee = new TeeClient(teeUrl);
|
|
3780
|
+
await tee.enrolAuthShare(jwt, shareSet.recovery);
|
|
3781
|
+
break;
|
|
3782
|
+
} catch (e) {
|
|
3783
|
+
if (attempt === 2) {
|
|
3784
|
+
console.warn(
|
|
3785
|
+
"decane-connect-kit: could not enrol this wallet for server-assisted recovery; a recovery file will be required on a new device. This wallet will get a 409 not_enrolled from /signer/provision until it is re-enrolled.",
|
|
3786
|
+
e
|
|
3787
|
+
);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3699
3792
|
if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
|
|
3700
3793
|
try {
|
|
3701
|
-
const choice = await
|
|
3702
|
-
|
|
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) {
|
|
3703
3803
|
const file = await buildRecoveryShareFile(
|
|
3704
3804
|
shareSet.recovery,
|
|
3705
3805
|
choice.password,
|
|
@@ -3714,17 +3814,6 @@ async function createDecaneConnect(config) {
|
|
|
3714
3814
|
console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
|
|
3715
3815
|
}
|
|
3716
3816
|
}
|
|
3717
|
-
if (serverAssistedRecovery) {
|
|
3718
|
-
try {
|
|
3719
|
-
const tee = new TeeClient(teeUrl);
|
|
3720
|
-
await tee.enrolAuthShare(jwt, shareSet.recovery);
|
|
3721
|
-
} catch (e) {
|
|
3722
|
-
console.warn(
|
|
3723
|
-
"decane-connect-kit: could not enrol this wallet for server-assisted recovery; a recovery file will be required on a new device.",
|
|
3724
|
-
e
|
|
3725
|
-
);
|
|
3726
|
-
}
|
|
3727
|
-
}
|
|
3728
3817
|
zeroize(shareSet.recovery);
|
|
3729
3818
|
return addresses;
|
|
3730
3819
|
}
|
|
@@ -3750,6 +3839,9 @@ async function createDecaneConnect(config) {
|
|
|
3750
3839
|
}
|
|
3751
3840
|
return addrs;
|
|
3752
3841
|
}
|
|
3842
|
+
console.warn(
|
|
3843
|
+
"decane-connect-kit: this wallet is not enrolled for server-assisted provisioning (/signer/provision returned 409 not_enrolled), so a device share cannot be issued from the JWT alone. Falling back to passkey sync, then the recovery file."
|
|
3844
|
+
);
|
|
3753
3845
|
} catch (e) {
|
|
3754
3846
|
console.warn(
|
|
3755
3847
|
"decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
|
|
@@ -3810,6 +3902,7 @@ async function createDecaneConnect(config) {
|
|
|
3810
3902
|
}
|
|
3811
3903
|
throw e;
|
|
3812
3904
|
}
|
|
3905
|
+
void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
|
|
3813
3906
|
zeroize(deviceShare);
|
|
3814
3907
|
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3815
3908
|
console.warn(
|
|
@@ -7255,6 +7348,7 @@ function useSignTypedData() {
|
|
|
7255
7348
|
NewDeviceError,
|
|
7256
7349
|
OriginNotAllowedError,
|
|
7257
7350
|
PromptPinNotConfiguredError,
|
|
7351
|
+
ProvisioningTimeoutError,
|
|
7258
7352
|
ReconnectRequiredError,
|
|
7259
7353
|
RecoveryFailedError,
|
|
7260
7354
|
RecoveryWalletMismatchError,
|