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.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,12 +2417,17 @@ 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) })
2402
2424
  });
2403
- if (!res.ok) throw new Error("auth share enrolment failed");
2425
+ if (!res.ok) {
2426
+ const detail = await res.text().catch(() => "");
2427
+ throw new Error(
2428
+ `auth share enrolment failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
2429
+ );
2430
+ }
2404
2431
  }
2405
2432
  /**
2406
2433
  * Ask the enclave for a fresh device share for this authenticated user.
@@ -2413,12 +2440,17 @@ var TeeClient = class {
2413
2440
  */
2414
2441
  async provisionDeviceShare(jwt) {
2415
2442
  await this.ensureAttested();
2416
- const res = await fetch(`${this.teeUrl}/signer/provision`, {
2443
+ const res = await teeFetch(`${this.teeUrl}/signer/provision`, {
2417
2444
  method: "POST",
2418
2445
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
2419
2446
  });
2420
2447
  if (res.status === 409) return null;
2421
- if (!res.ok) throw new Error("device share provisioning failed");
2448
+ if (!res.ok) {
2449
+ const detail = await res.text().catch(() => "");
2450
+ throw new Error(
2451
+ `device share provisioning failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
2452
+ );
2453
+ }
2422
2454
  const body = await res.json();
2423
2455
  return base64ToBytes(body.deviceShare);
2424
2456
  }
@@ -2426,7 +2458,7 @@ var TeeClient = class {
2426
2458
  async rotateShares(jwt) {
2427
2459
  if (!this.handle) throw new Error("no session");
2428
2460
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2429
- const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2461
+ const res = await teeFetch(`${this.teeUrl}/signer/rotate`, {
2430
2462
  method: "POST",
2431
2463
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2432
2464
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2443,7 +2475,7 @@ var TeeClient = class {
2443
2475
  async exportPortableBackup(jwt) {
2444
2476
  if (!this.handle) throw new Error("no session");
2445
2477
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2446
- const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2478
+ const res = await teeFetch(`${this.teeUrl}/signer/export-portable`, {
2447
2479
  method: "POST",
2448
2480
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2449
2481
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2473,7 +2505,7 @@ var TeeClient = class {
2473
2505
  if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
2474
2506
  gate = await this.completeAssertion(jwt);
2475
2507
  }
2476
- const res = await fetch(`${this.teeUrl}/signer/sign`, {
2508
+ const res = await teeFetch(`${this.teeUrl}/signer/sign`, {
2477
2509
  method: "POST",
2478
2510
  headers: { "Content-Type": "application/json" },
2479
2511
  body: JSON.stringify({
@@ -3414,6 +3446,44 @@ async function createDecaneConnect(config) {
3414
3446
  let ethProvider = null;
3415
3447
  const persistSession = config.persistSession !== false;
3416
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
+ }
3417
3487
  const SESSION_KEY = sessionPersistKey(config.apiKey);
3418
3488
  function persistLiveSession() {
3419
3489
  if (!persistSession) return;
@@ -3554,6 +3624,8 @@ async function createDecaneConnect(config) {
3554
3624
  ({ wrappingKey, meta } = await pinSetup());
3555
3625
  unlockMethod = "pin";
3556
3626
  }
3627
+ const provisioningStartedAt = Date.now();
3628
+ const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
3557
3629
  const master = generateMaster();
3558
3630
  let shareSet;
3559
3631
  let evmAddress;
@@ -3585,7 +3657,7 @@ async function createDecaneConnect(config) {
3585
3657
  version: 1
3586
3658
  });
3587
3659
  const serverIv = generateRandomBytes(12);
3588
- await api.putShare(jwt, {
3660
+ await withDeadline(api.putShare(jwt, {
3589
3661
  ciphertext: shareSet.server,
3590
3662
  iv: serverIv,
3591
3663
  wrappedShareMeta: {
@@ -3595,7 +3667,7 @@ async function createDecaneConnect(config) {
3595
3667
  evmAddress,
3596
3668
  solanaAddress,
3597
3669
  tronAddress
3598
- });
3670
+ }), remainingBudget(), "storing your wallet");
3599
3671
  zeroize(shareSet.server);
3600
3672
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
3601
3673
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
@@ -3611,7 +3683,11 @@ async function createDecaneConnect(config) {
3611
3683
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
3612
3684
  }
3613
3685
  }
3614
- 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
+ );
3615
3691
  zeroize(shareSet.device);
3616
3692
  if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3617
3693
  console.warn(
@@ -3621,10 +3697,33 @@ async function createDecaneConnect(config) {
3621
3697
  addresses.evm = evmAddress = confirmed.evm;
3622
3698
  addresses.solana = solanaAddress = confirmed.solana;
3623
3699
  addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
3700
+ 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
+ }
3715
+ }
3624
3716
  if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
3625
3717
  try {
3626
- const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3627
- 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) {
3628
3727
  const file = await buildRecoveryShareFile(
3629
3728
  shareSet.recovery,
3630
3729
  choice.password,
@@ -3639,17 +3738,6 @@ async function createDecaneConnect(config) {
3639
3738
  console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
3640
3739
  }
3641
3740
  }
3642
- if (serverAssistedRecovery) {
3643
- try {
3644
- const tee = new TeeClient(teeUrl);
3645
- await tee.enrolAuthShare(jwt, shareSet.recovery);
3646
- } catch (e) {
3647
- console.warn(
3648
- "decane-connect-kit: could not enrol this wallet for server-assisted recovery; a recovery file will be required on a new device.",
3649
- e
3650
- );
3651
- }
3652
- }
3653
3741
  zeroize(shareSet.recovery);
3654
3742
  return addresses;
3655
3743
  }
@@ -3675,6 +3763,9 @@ async function createDecaneConnect(config) {
3675
3763
  }
3676
3764
  return addrs;
3677
3765
  }
3766
+ console.warn(
3767
+ "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."
3768
+ );
3678
3769
  } catch (e) {
3679
3770
  console.warn(
3680
3771
  "decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
@@ -3735,6 +3826,7 @@ async function createDecaneConnect(config) {
3735
3826
  }
3736
3827
  throw e;
3737
3828
  }
3829
+ void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
3738
3830
  zeroize(deviceShare);
3739
3831
  if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3740
3832
  console.warn(
@@ -7186,6 +7278,7 @@ export {
7186
7278
  NewDeviceError,
7187
7279
  OriginNotAllowedError,
7188
7280
  PromptPinNotConfiguredError,
7281
+ ProvisioningTimeoutError,
7189
7282
  ReconnectRequiredError,
7190
7283
  RecoveryFailedError,
7191
7284
  RecoveryWalletMismatchError,