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 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 fetch(`${this.teeUrl}/signer/session`, {
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 fetch(`${this.teeUrl}/signer/recover`, {
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 fetch(`${this.teeUrl}/signer/assertion-challenge`, {
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 fetch(`${this.teeUrl}/signer/auth-share`, {
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 fetch(`${this.teeUrl}/signer/provision`, {
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 fetch(`${this.teeUrl}/signer/rotate`, {
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 fetch(`${this.teeUrl}/signer/export-portable`, {
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 fetch(`${this.teeUrl}/signer/sign`, {
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 session.start(shareSet.device, jwt, addresses);
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 config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3728
- if (choice.wants) {
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,