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.d.cts CHANGED
@@ -256,6 +256,13 @@ declare class ApiClient {
256
256
  savedAt: string | null;
257
257
  nagged: boolean;
258
258
  shareSetVersion: number;
259
+ /**
260
+ * Whether this wallet is enrolled for server-assisted provisioning, i.e.
261
+ * whether a new device can be given a device share from login alone.
262
+ * Optional because backends older than this field simply omit it — treat
263
+ * `undefined` as "unknown", never as "not enrolled".
264
+ */
265
+ enrolled?: boolean;
259
266
  }>;
260
267
  dismissRecoveryNag(jwt: string): Promise<void>;
261
268
  }
@@ -521,7 +528,16 @@ interface SignTypedDataRequest {
521
528
  message: Record<string, unknown>;
522
529
  primaryType?: string;
523
530
  }
524
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking';
531
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
532
+ /**
533
+ * Emitted when this wallet could not be enrolled for server-assisted
534
+ * recovery. Not fatal — the wallet works — but until it is enrolled the user
535
+ * cannot get a device share from login alone, so a future new device will
536
+ * need a passkey backup or a recovery file. The SDK retries automatically on
537
+ * later unlocks; surface or report this if you want visibility rather than a
538
+ * console line.
539
+ */
540
+ | 'enrolment-failed';
525
541
  interface DecaneConnect {
526
542
  connect(): Promise<ConnectResult>;
527
543
  connectWithGoogle(): Promise<ConnectResult>;
@@ -808,6 +824,20 @@ declare class NetworkError extends DecaneConnectError {
808
824
  declare class ShareCorruptedError extends DecaneConnectError {
809
825
  constructor();
810
826
  }
827
+ /**
828
+ * Wallet creation exceeded its time budget.
829
+ *
830
+ * Provisioning is bounded so a stalled network or an unresponsive enclave
831
+ * surfaces as an error the app can act on, rather than a signup that appears to
832
+ * hang forever. The budget covers only the SDK's own work — key generation,
833
+ * share storage, opening the signer session — never time spent waiting on the
834
+ * user (a passkey prompt, choosing a PIN, the recovery-file dialog).
835
+ *
836
+ * Retryable: nothing is left half-created that a later sign-in cannot complete.
837
+ */
838
+ declare class ProvisioningTimeoutError extends DecaneConnectError {
839
+ constructor(step: string);
840
+ }
811
841
  declare class NewDeviceError extends DecaneConnectError {
812
842
  constructor();
813
843
  }
@@ -1238,4 +1268,4 @@ declare function useSignTypedData(): {
1238
1268
  reset: () => void;
1239
1269
  };
1240
1270
 
1241
- export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPinNotConfiguredError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UserCancelledError, WalletSelector, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, verifyEmailCode };
1271
+ export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UserCancelledError, WalletSelector, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, verifyEmailCode };
package/dist/index.d.ts CHANGED
@@ -256,6 +256,13 @@ declare class ApiClient {
256
256
  savedAt: string | null;
257
257
  nagged: boolean;
258
258
  shareSetVersion: number;
259
+ /**
260
+ * Whether this wallet is enrolled for server-assisted provisioning, i.e.
261
+ * whether a new device can be given a device share from login alone.
262
+ * Optional because backends older than this field simply omit it — treat
263
+ * `undefined` as "unknown", never as "not enrolled".
264
+ */
265
+ enrolled?: boolean;
259
266
  }>;
260
267
  dismissRecoveryNag(jwt: string): Promise<void>;
261
268
  }
@@ -521,7 +528,16 @@ interface SignTypedDataRequest {
521
528
  message: Record<string, unknown>;
522
529
  primaryType?: string;
523
530
  }
524
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking';
531
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
532
+ /**
533
+ * Emitted when this wallet could not be enrolled for server-assisted
534
+ * recovery. Not fatal — the wallet works — but until it is enrolled the user
535
+ * cannot get a device share from login alone, so a future new device will
536
+ * need a passkey backup or a recovery file. The SDK retries automatically on
537
+ * later unlocks; surface or report this if you want visibility rather than a
538
+ * console line.
539
+ */
540
+ | 'enrolment-failed';
525
541
  interface DecaneConnect {
526
542
  connect(): Promise<ConnectResult>;
527
543
  connectWithGoogle(): Promise<ConnectResult>;
@@ -808,6 +824,20 @@ declare class NetworkError extends DecaneConnectError {
808
824
  declare class ShareCorruptedError extends DecaneConnectError {
809
825
  constructor();
810
826
  }
827
+ /**
828
+ * Wallet creation exceeded its time budget.
829
+ *
830
+ * Provisioning is bounded so a stalled network or an unresponsive enclave
831
+ * surfaces as an error the app can act on, rather than a signup that appears to
832
+ * hang forever. The budget covers only the SDK's own work — key generation,
833
+ * share storage, opening the signer session — never time spent waiting on the
834
+ * user (a passkey prompt, choosing a PIN, the recovery-file dialog).
835
+ *
836
+ * Retryable: nothing is left half-created that a later sign-in cannot complete.
837
+ */
838
+ declare class ProvisioningTimeoutError extends DecaneConnectError {
839
+ constructor(step: string);
840
+ }
811
841
  declare class NewDeviceError extends DecaneConnectError {
812
842
  constructor();
813
843
  }
@@ -1238,4 +1268,4 @@ declare function useSignTypedData(): {
1238
1268
  reset: () => void;
1239
1269
  };
1240
1270
 
1241
- export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPinNotConfiguredError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UserCancelledError, WalletSelector, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, verifyEmailCode };
1271
+ export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UserCancelledError, WalletSelector, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, verifyEmailCode };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ __export(errors_exports, {
22
22
  NewDeviceError: () => NewDeviceError,
23
23
  OriginNotAllowedError: () => OriginNotAllowedError,
24
24
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
25
+ ProvisioningTimeoutError: () => ProvisioningTimeoutError,
25
26
  ReconnectRequiredError: () => ReconnectRequiredError,
26
27
  RecoveryFailedError: () => RecoveryFailedError,
27
28
  RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
@@ -33,7 +34,7 @@ __export(errors_exports, {
33
34
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
34
35
  WrongPasswordError: () => WrongPasswordError
35
36
  });
36
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
37
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
37
38
  var init_errors = __esm({
38
39
  "src/social/errors.ts"() {
39
40
  "use strict";
@@ -104,6 +105,11 @@ var init_errors = __esm({
104
105
  super("SHARE_CORRUPTED", "Stored device share is corrupted");
105
106
  }
106
107
  };
108
+ ProvisioningTimeoutError = class extends DecaneConnectError {
109
+ constructor(step) {
110
+ super("PROVISIONING_TIMEOUT", `Wallet setup timed out while ${step}. Check your connection and try again.`, true);
111
+ }
112
+ };
107
113
  NewDeviceError = class extends DecaneConnectError {
108
114
  constructor() {
109
115
  super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
@@ -1932,6 +1938,7 @@ var ERROR_MAP = {
1932
1938
  // generic failure message.
1933
1939
  UNAUTHORIZED: ReconnectRequiredError
1934
1940
  };
1941
+ var REQUEST_TIMEOUT_MS = 3e4;
1935
1942
  var ApiClient = class {
1936
1943
  constructor(baseUrl, apiKey, appId) {
1937
1944
  this.baseUrl = baseUrl;
@@ -1940,9 +1947,12 @@ var ApiClient = class {
1940
1947
  }
1941
1948
  async _request(path, init) {
1942
1949
  let res;
1950
+ const controller = new AbortController();
1951
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1943
1952
  try {
1944
1953
  res = await fetch(`${this.baseUrl}${path}`, {
1945
1954
  ...init,
1955
+ signal: controller.signal,
1946
1956
  headers: {
1947
1957
  "Content-Type": "application/json",
1948
1958
  "X-API-Key": this.apiKey,
@@ -1952,6 +1962,8 @@ var ApiClient = class {
1952
1962
  });
1953
1963
  } catch {
1954
1964
  throw new NetworkError();
1965
+ } finally {
1966
+ clearTimeout(timer);
1955
1967
  }
1956
1968
  if (res.status === 404 && init?.allow404) return null;
1957
1969
  if (!res.ok) {
@@ -2254,6 +2266,16 @@ function bytesToBase64(b) {
2254
2266
  function base64ToBytes(s) {
2255
2267
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2256
2268
  }
2269
+ var TEE_TIMEOUT_MS = 3e4;
2270
+ async function teeFetch(url, init) {
2271
+ const controller = new AbortController();
2272
+ const timer = setTimeout(() => controller.abort(), TEE_TIMEOUT_MS);
2273
+ try {
2274
+ return await fetch(url, { ...init, signal: controller.signal });
2275
+ } finally {
2276
+ clearTimeout(timer);
2277
+ }
2278
+ }
2257
2279
  var TeeClient = class {
2258
2280
  constructor(teeUrl) {
2259
2281
  this.teeUrl = teeUrl;
@@ -2309,7 +2331,7 @@ var TeeClient = class {
2309
2331
  */
2310
2332
  async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
2311
2333
  await this.ensureAttested();
2312
- const res = await fetch(`${this.teeUrl}/signer/session`, {
2334
+ const res = await teeFetch(`${this.teeUrl}/signer/session`, {
2313
2335
  method: "POST",
2314
2336
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2315
2337
  body: JSON.stringify({
@@ -2341,7 +2363,7 @@ var TeeClient = class {
2341
2363
  */
2342
2364
  async recoverWithShare(jwt, recoveryShare) {
2343
2365
  await this.ensureAttested();
2344
- const res = await fetch(`${this.teeUrl}/signer/recover`, {
2366
+ const res = await teeFetch(`${this.teeUrl}/signer/recover`, {
2345
2367
  method: "POST",
2346
2368
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2347
2369
  body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
@@ -2366,7 +2388,7 @@ var TeeClient = class {
2366
2388
  * require proof of fresh authenticator possession, not just an active session.
2367
2389
  */
2368
2390
  async completeAssertion(jwt) {
2369
- const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2391
+ const res = await teeFetch(`${this.teeUrl}/signer/assertion-challenge`, {
2370
2392
  method: "POST",
2371
2393
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2372
2394
  body: JSON.stringify({ rpId: window.location.hostname })
@@ -2395,7 +2417,7 @@ var TeeClient = class {
2395
2417
  */
2396
2418
  async enrolAuthShare(jwt, recoveryShare) {
2397
2419
  await this.ensureAttested();
2398
- const res = await fetch(`${this.teeUrl}/signer/auth-share`, {
2420
+ const res = await teeFetch(`${this.teeUrl}/signer/auth-share`, {
2399
2421
  method: "POST",
2400
2422
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2401
2423
  body: JSON.stringify({ share: bytesToBase64(recoveryShare) })
@@ -2418,7 +2440,7 @@ var TeeClient = class {
2418
2440
  */
2419
2441
  async provisionDeviceShare(jwt) {
2420
2442
  await this.ensureAttested();
2421
- const res = await fetch(`${this.teeUrl}/signer/provision`, {
2443
+ const res = await teeFetch(`${this.teeUrl}/signer/provision`, {
2422
2444
  method: "POST",
2423
2445
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
2424
2446
  });
@@ -2436,7 +2458,7 @@ var TeeClient = class {
2436
2458
  async rotateShares(jwt) {
2437
2459
  if (!this.handle) throw new Error("no session");
2438
2460
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2439
- const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2461
+ const res = await teeFetch(`${this.teeUrl}/signer/rotate`, {
2440
2462
  method: "POST",
2441
2463
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2442
2464
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2453,7 +2475,7 @@ var TeeClient = class {
2453
2475
  async exportPortableBackup(jwt) {
2454
2476
  if (!this.handle) throw new Error("no session");
2455
2477
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2456
- const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2478
+ const res = await teeFetch(`${this.teeUrl}/signer/export-portable`, {
2457
2479
  method: "POST",
2458
2480
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2459
2481
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2483,7 +2505,7 @@ var TeeClient = class {
2483
2505
  if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
2484
2506
  gate = await this.completeAssertion(jwt);
2485
2507
  }
2486
- const res = await fetch(`${this.teeUrl}/signer/sign`, {
2508
+ const res = await teeFetch(`${this.teeUrl}/signer/sign`, {
2487
2509
  method: "POST",
2488
2510
  headers: { "Content-Type": "application/json" },
2489
2511
  body: JSON.stringify({
@@ -3424,6 +3446,44 @@ async function createDecaneConnect(config) {
3424
3446
  let ethProvider = null;
3425
3447
  const persistSession = config.persistSession !== false;
3426
3448
  const serverAssistedRecovery = true;
3449
+ const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3450
+ const selfHealAttempted = /* @__PURE__ */ new Set();
3451
+ async function selfHealEnrolment(userId, jwt, share) {
3452
+ try {
3453
+ if (selfHealAttempted.has(userId)) return;
3454
+ const status = await api.getRecoveryStatus(jwt);
3455
+ if (status.enrolled !== false) return;
3456
+ selfHealAttempted.add(userId);
3457
+ await new TeeClient(teeUrl).enrolAuthShare(jwt, share);
3458
+ } catch (e) {
3459
+ events.emit("enrolment-failed");
3460
+ console.warn(
3461
+ "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.",
3462
+ e
3463
+ );
3464
+ } finally {
3465
+ zeroize(share);
3466
+ }
3467
+ }
3468
+ const PROVISIONING_BUDGET_MS = 9e4;
3469
+ function withDeadline(promise, ms, step) {
3470
+ let timer;
3471
+ return Promise.race([
3472
+ promise,
3473
+ new Promise((_, reject) => {
3474
+ timer = setTimeout(() => reject(new ProvisioningTimeoutError(step)), Math.max(0, ms));
3475
+ })
3476
+ ]).finally(() => clearTimeout(timer));
3477
+ }
3478
+ function withTimeout(promise, ms) {
3479
+ let timer;
3480
+ return Promise.race([
3481
+ promise,
3482
+ new Promise((resolve) => {
3483
+ timer = setTimeout(() => resolve(null), ms);
3484
+ })
3485
+ ]).finally(() => clearTimeout(timer));
3486
+ }
3427
3487
  const SESSION_KEY = sessionPersistKey(config.apiKey);
3428
3488
  function persistLiveSession() {
3429
3489
  if (!persistSession) return;
@@ -3564,6 +3624,8 @@ async function createDecaneConnect(config) {
3564
3624
  ({ wrappingKey, meta } = await pinSetup());
3565
3625
  unlockMethod = "pin";
3566
3626
  }
3627
+ const provisioningStartedAt = Date.now();
3628
+ const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
3567
3629
  const master = generateMaster();
3568
3630
  let shareSet;
3569
3631
  let evmAddress;
@@ -3595,7 +3657,7 @@ async function createDecaneConnect(config) {
3595
3657
  version: 1
3596
3658
  });
3597
3659
  const serverIv = generateRandomBytes(12);
3598
- await api.putShare(jwt, {
3660
+ await withDeadline(api.putShare(jwt, {
3599
3661
  ciphertext: shareSet.server,
3600
3662
  iv: serverIv,
3601
3663
  wrappedShareMeta: {
@@ -3605,7 +3667,7 @@ async function createDecaneConnect(config) {
3605
3667
  evmAddress,
3606
3668
  solanaAddress,
3607
3669
  tronAddress
3608
- });
3670
+ }), remainingBudget(), "storing your wallet");
3609
3671
  zeroize(shareSet.server);
3610
3672
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
3611
3673
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
@@ -3621,7 +3683,11 @@ async function createDecaneConnect(config) {
3621
3683
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
3622
3684
  }
3623
3685
  }
3624
- const confirmed = await session.start(shareSet.device, jwt, addresses);
3686
+ const confirmed = await withDeadline(
3687
+ session.start(shareSet.device, jwt, addresses),
3688
+ remainingBudget(),
3689
+ "opening the signing session"
3690
+ );
3625
3691
  zeroize(shareSet.device);
3626
3692
  if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3627
3693
  console.warn(
@@ -3649,8 +3715,15 @@ async function createDecaneConnect(config) {
3649
3715
  }
3650
3716
  if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
3651
3717
  try {
3652
- const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3653
- if (choice.wants) {
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) {
3654
3727
  const file = await buildRecoveryShareFile(
3655
3728
  shareSet.recovery,
3656
3729
  choice.password,
@@ -3753,6 +3826,7 @@ async function createDecaneConnect(config) {
3753
3826
  }
3754
3827
  throw e;
3755
3828
  }
3829
+ void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
3756
3830
  zeroize(deviceShare);
3757
3831
  if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3758
3832
  console.warn(
@@ -7204,6 +7278,7 @@ export {
7204
7278
  NewDeviceError,
7205
7279
  OriginNotAllowedError,
7206
7280
  PromptPinNotConfiguredError,
7281
+ ProvisioningTimeoutError,
7207
7282
  ReconnectRequiredError,
7208
7283
  RecoveryFailedError,
7209
7284
  RecoveryWalletMismatchError,