decane-connect-kit 2.7.3 → 2.7.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/dist/index.cjs +84 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +84 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -417,7 +417,22 @@ interface DecaneConnectConfig {
|
|
|
417
417
|
* page load (stricter, but the reload friction that motivated this returns).
|
|
418
418
|
*/
|
|
419
419
|
persistSession?: boolean;
|
|
420
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Offer to save a recovery-share file at signup. **Default false** since
|
|
422
|
+
* 2.7.4 — opt in explicitly with `true`.
|
|
423
|
+
*
|
|
424
|
+
* The offer existed because a new device could not otherwise get a device
|
|
425
|
+
* share. Server-assisted provisioning now covers that case: every wallet is
|
|
426
|
+
* enrolled at creation, un-enrolled wallets repair themselves on the next
|
|
427
|
+
* unlock, and a returning user is provisioned from their sign-in alone. For
|
|
428
|
+
* almost every app the dialog is now a password prompt during onboarding that
|
|
429
|
+
* buys nothing, so it is off unless asked for.
|
|
430
|
+
*
|
|
431
|
+
* Recovery files themselves are unchanged and still work — `rotateShares()`
|
|
432
|
+
* issues one on demand, and `promptForRecoveryFile` still accepts one during
|
|
433
|
+
* recovery. Turn this back on if you want users to hold an offline fallback
|
|
434
|
+
* that does not depend on Decane being reachable.
|
|
435
|
+
*/
|
|
421
436
|
offerRecoveryAtSignup?: boolean;
|
|
422
437
|
/** Ask the user whether they want a recovery share and, if so, a password for it. */
|
|
423
438
|
onRecoveryShareOffer?: (addresses: {
|
package/dist/index.d.ts
CHANGED
|
@@ -417,7 +417,22 @@ interface DecaneConnectConfig {
|
|
|
417
417
|
* page load (stricter, but the reload friction that motivated this returns).
|
|
418
418
|
*/
|
|
419
419
|
persistSession?: boolean;
|
|
420
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Offer to save a recovery-share file at signup. **Default false** since
|
|
422
|
+
* 2.7.4 — opt in explicitly with `true`.
|
|
423
|
+
*
|
|
424
|
+
* The offer existed because a new device could not otherwise get a device
|
|
425
|
+
* share. Server-assisted provisioning now covers that case: every wallet is
|
|
426
|
+
* enrolled at creation, un-enrolled wallets repair themselves on the next
|
|
427
|
+
* unlock, and a returning user is provisioned from their sign-in alone. For
|
|
428
|
+
* almost every app the dialog is now a password prompt during onboarding that
|
|
429
|
+
* buys nothing, so it is off unless asked for.
|
|
430
|
+
*
|
|
431
|
+
* Recovery files themselves are unchanged and still work — `rotateShares()`
|
|
432
|
+
* issues one on demand, and `promptForRecoveryFile` still accepts one during
|
|
433
|
+
* recovery. Turn this back on if you want users to hold an offline fallback
|
|
434
|
+
* that does not depend on Decane being reachable.
|
|
435
|
+
*/
|
|
421
436
|
offerRecoveryAtSignup?: boolean;
|
|
422
437
|
/** Ask the user whether they want a recovery share and, if so, a password for it. */
|
|
423
438
|
onRecoveryShareOffer?: (addresses: {
|
package/dist/index.js
CHANGED
|
@@ -2276,6 +2276,26 @@ async function teeFetch(url, init) {
|
|
|
2276
2276
|
clearTimeout(timer);
|
|
2277
2277
|
}
|
|
2278
2278
|
}
|
|
2279
|
+
var ATTESTATION_TTL_MS = 10 * 6e4;
|
|
2280
|
+
var attestationCache = /* @__PURE__ */ new Map();
|
|
2281
|
+
function attestOnce(teeUrl, verify) {
|
|
2282
|
+
const hit = attestationCache.get(teeUrl);
|
|
2283
|
+
if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
|
|
2284
|
+
const promise = verify().catch((e) => {
|
|
2285
|
+
attestationCache.delete(teeUrl);
|
|
2286
|
+
throw e;
|
|
2287
|
+
});
|
|
2288
|
+
attestationCache.set(teeUrl, { at: Date.now(), promise });
|
|
2289
|
+
return promise;
|
|
2290
|
+
}
|
|
2291
|
+
function prewarmAttestation(teeUrl) {
|
|
2292
|
+
void attestOnce(teeUrl, async () => {
|
|
2293
|
+
const nonce = crypto.getRandomValues(new Uint8Array(16));
|
|
2294
|
+
const nonceHex = Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2295
|
+
await verifyTeeAttestation(teeUrl, nonceHex);
|
|
2296
|
+
}).catch(() => {
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2279
2299
|
var TeeClient = class {
|
|
2280
2300
|
constructor(teeUrl) {
|
|
2281
2301
|
this.teeUrl = teeUrl;
|
|
@@ -2317,9 +2337,11 @@ var TeeClient = class {
|
|
|
2317
2337
|
}
|
|
2318
2338
|
async ensureAttested() {
|
|
2319
2339
|
if (this.attested) return;
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2340
|
+
await attestOnce(this.teeUrl, async () => {
|
|
2341
|
+
const nonce = crypto.getRandomValues(new Uint8Array(16));
|
|
2342
|
+
const nonceHex = Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2343
|
+
await verifyTeeAttestation(this.teeUrl, nonceHex);
|
|
2344
|
+
});
|
|
2323
2345
|
this.attested = true;
|
|
2324
2346
|
}
|
|
2325
2347
|
/**
|
|
@@ -3465,6 +3487,26 @@ async function createDecaneConnect(config) {
|
|
|
3465
3487
|
zeroize(share);
|
|
3466
3488
|
}
|
|
3467
3489
|
}
|
|
3490
|
+
async function enrolInBackground(jwt, share) {
|
|
3491
|
+
try {
|
|
3492
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
3493
|
+
try {
|
|
3494
|
+
await new TeeClient(teeUrl).enrolAuthShare(jwt, share);
|
|
3495
|
+
return;
|
|
3496
|
+
} catch (e) {
|
|
3497
|
+
if (attempt === 2) {
|
|
3498
|
+
events.emit("enrolment-failed");
|
|
3499
|
+
console.warn(
|
|
3500
|
+
"decane-connect-kit: could not enrol this wallet for server-assisted recovery. The wallet is fine and fully usable; it will be re-enrolled automatically on the next unlock. Until then a brand-new device would need a passkey backup or a recovery file.",
|
|
3501
|
+
e
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
} finally {
|
|
3507
|
+
zeroize(share);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3468
3510
|
const PROVISIONING_BUDGET_MS = 9e4;
|
|
3469
3511
|
function withDeadline(promise, ms, step) {
|
|
3470
3512
|
let timer;
|
|
@@ -3595,6 +3637,7 @@ async function createDecaneConnect(config) {
|
|
|
3595
3637
|
}
|
|
3596
3638
|
}
|
|
3597
3639
|
async function firstTimeSetup(userId, jwt) {
|
|
3640
|
+
prewarmAttestation(teeUrl);
|
|
3598
3641
|
let unlockMethod;
|
|
3599
3642
|
let wrappingKey;
|
|
3600
3643
|
let meta = {};
|
|
@@ -3698,49 +3741,49 @@ async function createDecaneConnect(config) {
|
|
|
3698
3741
|
addresses.solana = solanaAddress = confirmed.solana;
|
|
3699
3742
|
addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
|
|
3700
3743
|
if (serverAssistedRecovery) {
|
|
3701
|
-
|
|
3702
|
-
try {
|
|
3703
|
-
const tee = new TeeClient(teeUrl);
|
|
3704
|
-
await tee.enrolAuthShare(jwt, shareSet.recovery);
|
|
3705
|
-
break;
|
|
3706
|
-
} catch (e) {
|
|
3707
|
-
if (attempt === 2) {
|
|
3708
|
-
console.warn(
|
|
3709
|
-
"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.",
|
|
3710
|
-
e
|
|
3711
|
-
);
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
}
|
|
3744
|
+
void enrolInBackground(jwt, new Uint8Array(shareSet.recovery));
|
|
3715
3745
|
}
|
|
3716
|
-
if (config.offerRecoveryAtSignup
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
);
|
|
3726
|
-
} else if (choice.wants) {
|
|
3727
|
-
const file = await buildRecoveryShareFile(
|
|
3728
|
-
shareSet.recovery,
|
|
3729
|
-
choice.password,
|
|
3730
|
-
addresses,
|
|
3731
|
-
shareSet.version,
|
|
3732
|
-
choice.passwordHint
|
|
3733
|
-
);
|
|
3734
|
-
await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
|
|
3735
|
-
await api.markRecoveryShareSaved(jwt);
|
|
3736
|
-
}
|
|
3737
|
-
} catch (e) {
|
|
3738
|
-
console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
|
|
3739
|
-
}
|
|
3746
|
+
if (config.offerRecoveryAtSignup === true && config.onRecoveryShareOffer) {
|
|
3747
|
+
void offerRecoveryShare(
|
|
3748
|
+
jwt,
|
|
3749
|
+
new Uint8Array(shareSet.recovery),
|
|
3750
|
+
{ ...addresses },
|
|
3751
|
+
shareSet.version,
|
|
3752
|
+
evmAddress,
|
|
3753
|
+
solanaAddress
|
|
3754
|
+
);
|
|
3740
3755
|
}
|
|
3741
3756
|
zeroize(shareSet.recovery);
|
|
3742
3757
|
return addresses;
|
|
3743
3758
|
}
|
|
3759
|
+
async function offerRecoveryShare(jwt, recoveryShare, addresses, shareSetVersion, evmAddress, solanaAddress) {
|
|
3760
|
+
try {
|
|
3761
|
+
const choice = await withTimeout(
|
|
3762
|
+
config.onRecoveryShareOffer({ evmAddress, solanaAddress }),
|
|
3763
|
+
RECOVERY_OFFER_TIMEOUT_MS
|
|
3764
|
+
);
|
|
3765
|
+
if (!choice) {
|
|
3766
|
+
console.warn(
|
|
3767
|
+
`decane-connect-kit: onRecoveryShareOffer did not settle within ${RECOVERY_OFFER_TIMEOUT_MS / 6e4} minutes, so no recovery file was offered. The wallet is unaffected \u2014 it was already created and enrolled \u2014 and the user can generate a recovery share later via rotateShares(). Check that your handler always resolves or rejects.`
|
|
3768
|
+
);
|
|
3769
|
+
return;
|
|
3770
|
+
}
|
|
3771
|
+
if (!choice.wants) return;
|
|
3772
|
+
const file = await buildRecoveryShareFile(
|
|
3773
|
+
recoveryShare,
|
|
3774
|
+
choice.password,
|
|
3775
|
+
addresses,
|
|
3776
|
+
shareSetVersion,
|
|
3777
|
+
choice.passwordHint
|
|
3778
|
+
);
|
|
3779
|
+
await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
|
|
3780
|
+
await api.markRecoveryShareSaved(jwt);
|
|
3781
|
+
} catch (e) {
|
|
3782
|
+
console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
|
|
3783
|
+
} finally {
|
|
3784
|
+
zeroize(recoveryShare);
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3744
3787
|
async function returningUserUnlock(userId, jwt) {
|
|
3745
3788
|
const rawAddresses = await api.getAddresses(jwt);
|
|
3746
3789
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|