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.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
- /** Offer to save a recovery-share file at signup. Default true. Never blocks onboarding. */
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
- /** Offer to save a recovery-share file at signup. Default true. Never blocks onboarding. */
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
- const nonce = crypto.getRandomValues(new Uint8Array(16));
2321
- const nonceHex = Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join("");
2322
- await verifyTeeAttestation(this.teeUrl, nonceHex);
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
- for (let attempt = 1; attempt <= 2; attempt++) {
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 !== false && config.onRecoveryShareOffer) {
3717
- try {
3718
- const choice = await withTimeout(
3719
- config.onRecoveryShareOffer({ evmAddress, solanaAddress }),
3720
- RECOVERY_OFFER_TIMEOUT_MS
3721
- );
3722
- if (!choice) {
3723
- console.warn(
3724
- `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.`
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;