decane-connect-kit 2.7.2 → 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
@@ -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
  }
@@ -410,7 +417,22 @@ interface DecaneConnectConfig {
410
417
  * page load (stricter, but the reload friction that motivated this returns).
411
418
  */
412
419
  persistSession?: boolean;
413
- /** 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
+ */
414
436
  offerRecoveryAtSignup?: boolean;
415
437
  /** Ask the user whether they want a recovery share and, if so, a password for it. */
416
438
  onRecoveryShareOffer?: (addresses: {
@@ -521,7 +543,16 @@ interface SignTypedDataRequest {
521
543
  message: Record<string, unknown>;
522
544
  primaryType?: string;
523
545
  }
524
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking';
546
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
547
+ /**
548
+ * Emitted when this wallet could not be enrolled for server-assisted
549
+ * recovery. Not fatal — the wallet works — but until it is enrolled the user
550
+ * cannot get a device share from login alone, so a future new device will
551
+ * need a passkey backup or a recovery file. The SDK retries automatically on
552
+ * later unlocks; surface or report this if you want visibility rather than a
553
+ * console line.
554
+ */
555
+ | 'enrolment-failed';
525
556
  interface DecaneConnect {
526
557
  connect(): Promise<ConnectResult>;
527
558
  connectWithGoogle(): Promise<ConnectResult>;
@@ -808,6 +839,20 @@ declare class NetworkError extends DecaneConnectError {
808
839
  declare class ShareCorruptedError extends DecaneConnectError {
809
840
  constructor();
810
841
  }
842
+ /**
843
+ * Wallet creation exceeded its time budget.
844
+ *
845
+ * Provisioning is bounded so a stalled network or an unresponsive enclave
846
+ * surfaces as an error the app can act on, rather than a signup that appears to
847
+ * hang forever. The budget covers only the SDK's own work — key generation,
848
+ * share storage, opening the signer session — never time spent waiting on the
849
+ * user (a passkey prompt, choosing a PIN, the recovery-file dialog).
850
+ *
851
+ * Retryable: nothing is left half-created that a later sign-in cannot complete.
852
+ */
853
+ declare class ProvisioningTimeoutError extends DecaneConnectError {
854
+ constructor(step: string);
855
+ }
811
856
  declare class NewDeviceError extends DecaneConnectError {
812
857
  constructor();
813
858
  }
@@ -1238,4 +1283,4 @@ declare function useSignTypedData(): {
1238
1283
  reset: () => void;
1239
1284
  };
1240
1285
 
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 };
1286
+ 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
  }
@@ -410,7 +417,22 @@ interface DecaneConnectConfig {
410
417
  * page load (stricter, but the reload friction that motivated this returns).
411
418
  */
412
419
  persistSession?: boolean;
413
- /** 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
+ */
414
436
  offerRecoveryAtSignup?: boolean;
415
437
  /** Ask the user whether they want a recovery share and, if so, a password for it. */
416
438
  onRecoveryShareOffer?: (addresses: {
@@ -521,7 +543,16 @@ interface SignTypedDataRequest {
521
543
  message: Record<string, unknown>;
522
544
  primaryType?: string;
523
545
  }
524
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking';
546
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
547
+ /**
548
+ * Emitted when this wallet could not be enrolled for server-assisted
549
+ * recovery. Not fatal — the wallet works — but until it is enrolled the user
550
+ * cannot get a device share from login alone, so a future new device will
551
+ * need a passkey backup or a recovery file. The SDK retries automatically on
552
+ * later unlocks; surface or report this if you want visibility rather than a
553
+ * console line.
554
+ */
555
+ | 'enrolment-failed';
525
556
  interface DecaneConnect {
526
557
  connect(): Promise<ConnectResult>;
527
558
  connectWithGoogle(): Promise<ConnectResult>;
@@ -808,6 +839,20 @@ declare class NetworkError extends DecaneConnectError {
808
839
  declare class ShareCorruptedError extends DecaneConnectError {
809
840
  constructor();
810
841
  }
842
+ /**
843
+ * Wallet creation exceeded its time budget.
844
+ *
845
+ * Provisioning is bounded so a stalled network or an unresponsive enclave
846
+ * surfaces as an error the app can act on, rather than a signup that appears to
847
+ * hang forever. The budget covers only the SDK's own work — key generation,
848
+ * share storage, opening the signer session — never time spent waiting on the
849
+ * user (a passkey prompt, choosing a PIN, the recovery-file dialog).
850
+ *
851
+ * Retryable: nothing is left half-created that a later sign-in cannot complete.
852
+ */
853
+ declare class ProvisioningTimeoutError extends DecaneConnectError {
854
+ constructor(step: string);
855
+ }
811
856
  declare class NewDeviceError extends DecaneConnectError {
812
857
  constructor();
813
858
  }
@@ -1238,4 +1283,4 @@ declare function useSignTypedData(): {
1238
1283
  reset: () => void;
1239
1284
  };
1240
1285
 
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 };
1286
+ 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,36 @@ 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
+ }
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
+ }
2257
2299
  var TeeClient = class {
2258
2300
  constructor(teeUrl) {
2259
2301
  this.teeUrl = teeUrl;
@@ -2295,9 +2337,11 @@ var TeeClient = class {
2295
2337
  }
2296
2338
  async ensureAttested() {
2297
2339
  if (this.attested) return;
2298
- const nonce = crypto.getRandomValues(new Uint8Array(16));
2299
- const nonceHex = Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join("");
2300
- 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
+ });
2301
2345
  this.attested = true;
2302
2346
  }
2303
2347
  /**
@@ -2309,7 +2353,7 @@ var TeeClient = class {
2309
2353
  */
2310
2354
  async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
2311
2355
  await this.ensureAttested();
2312
- const res = await fetch(`${this.teeUrl}/signer/session`, {
2356
+ const res = await teeFetch(`${this.teeUrl}/signer/session`, {
2313
2357
  method: "POST",
2314
2358
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2315
2359
  body: JSON.stringify({
@@ -2341,7 +2385,7 @@ var TeeClient = class {
2341
2385
  */
2342
2386
  async recoverWithShare(jwt, recoveryShare) {
2343
2387
  await this.ensureAttested();
2344
- const res = await fetch(`${this.teeUrl}/signer/recover`, {
2388
+ const res = await teeFetch(`${this.teeUrl}/signer/recover`, {
2345
2389
  method: "POST",
2346
2390
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2347
2391
  body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
@@ -2366,7 +2410,7 @@ var TeeClient = class {
2366
2410
  * require proof of fresh authenticator possession, not just an active session.
2367
2411
  */
2368
2412
  async completeAssertion(jwt) {
2369
- const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2413
+ const res = await teeFetch(`${this.teeUrl}/signer/assertion-challenge`, {
2370
2414
  method: "POST",
2371
2415
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2372
2416
  body: JSON.stringify({ rpId: window.location.hostname })
@@ -2395,7 +2439,7 @@ var TeeClient = class {
2395
2439
  */
2396
2440
  async enrolAuthShare(jwt, recoveryShare) {
2397
2441
  await this.ensureAttested();
2398
- const res = await fetch(`${this.teeUrl}/signer/auth-share`, {
2442
+ const res = await teeFetch(`${this.teeUrl}/signer/auth-share`, {
2399
2443
  method: "POST",
2400
2444
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2401
2445
  body: JSON.stringify({ share: bytesToBase64(recoveryShare) })
@@ -2418,7 +2462,7 @@ var TeeClient = class {
2418
2462
  */
2419
2463
  async provisionDeviceShare(jwt) {
2420
2464
  await this.ensureAttested();
2421
- const res = await fetch(`${this.teeUrl}/signer/provision`, {
2465
+ const res = await teeFetch(`${this.teeUrl}/signer/provision`, {
2422
2466
  method: "POST",
2423
2467
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
2424
2468
  });
@@ -2436,7 +2480,7 @@ var TeeClient = class {
2436
2480
  async rotateShares(jwt) {
2437
2481
  if (!this.handle) throw new Error("no session");
2438
2482
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2439
- const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2483
+ const res = await teeFetch(`${this.teeUrl}/signer/rotate`, {
2440
2484
  method: "POST",
2441
2485
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2442
2486
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2453,7 +2497,7 @@ var TeeClient = class {
2453
2497
  async exportPortableBackup(jwt) {
2454
2498
  if (!this.handle) throw new Error("no session");
2455
2499
  const { challengeId, assertion } = await this.completeAssertion(jwt);
2456
- const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2500
+ const res = await teeFetch(`${this.teeUrl}/signer/export-portable`, {
2457
2501
  method: "POST",
2458
2502
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2459
2503
  body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
@@ -2483,7 +2527,7 @@ var TeeClient = class {
2483
2527
  if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
2484
2528
  gate = await this.completeAssertion(jwt);
2485
2529
  }
2486
- const res = await fetch(`${this.teeUrl}/signer/sign`, {
2530
+ const res = await teeFetch(`${this.teeUrl}/signer/sign`, {
2487
2531
  method: "POST",
2488
2532
  headers: { "Content-Type": "application/json" },
2489
2533
  body: JSON.stringify({
@@ -3424,6 +3468,64 @@ async function createDecaneConnect(config) {
3424
3468
  let ethProvider = null;
3425
3469
  const persistSession = config.persistSession !== false;
3426
3470
  const serverAssistedRecovery = true;
3471
+ const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3472
+ const selfHealAttempted = /* @__PURE__ */ new Set();
3473
+ async function selfHealEnrolment(userId, jwt, share) {
3474
+ try {
3475
+ if (selfHealAttempted.has(userId)) return;
3476
+ const status = await api.getRecoveryStatus(jwt);
3477
+ if (status.enrolled !== false) return;
3478
+ selfHealAttempted.add(userId);
3479
+ await new TeeClient(teeUrl).enrolAuthShare(jwt, share);
3480
+ } catch (e) {
3481
+ events.emit("enrolment-failed");
3482
+ console.warn(
3483
+ "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.",
3484
+ e
3485
+ );
3486
+ } finally {
3487
+ zeroize(share);
3488
+ }
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
+ }
3510
+ const PROVISIONING_BUDGET_MS = 9e4;
3511
+ function withDeadline(promise, ms, step) {
3512
+ let timer;
3513
+ return Promise.race([
3514
+ promise,
3515
+ new Promise((_, reject) => {
3516
+ timer = setTimeout(() => reject(new ProvisioningTimeoutError(step)), Math.max(0, ms));
3517
+ })
3518
+ ]).finally(() => clearTimeout(timer));
3519
+ }
3520
+ function withTimeout(promise, ms) {
3521
+ let timer;
3522
+ return Promise.race([
3523
+ promise,
3524
+ new Promise((resolve) => {
3525
+ timer = setTimeout(() => resolve(null), ms);
3526
+ })
3527
+ ]).finally(() => clearTimeout(timer));
3528
+ }
3427
3529
  const SESSION_KEY = sessionPersistKey(config.apiKey);
3428
3530
  function persistLiveSession() {
3429
3531
  if (!persistSession) return;
@@ -3535,6 +3637,7 @@ async function createDecaneConnect(config) {
3535
3637
  }
3536
3638
  }
3537
3639
  async function firstTimeSetup(userId, jwt) {
3640
+ prewarmAttestation(teeUrl);
3538
3641
  let unlockMethod;
3539
3642
  let wrappingKey;
3540
3643
  let meta = {};
@@ -3564,6 +3667,8 @@ async function createDecaneConnect(config) {
3564
3667
  ({ wrappingKey, meta } = await pinSetup());
3565
3668
  unlockMethod = "pin";
3566
3669
  }
3670
+ const provisioningStartedAt = Date.now();
3671
+ const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
3567
3672
  const master = generateMaster();
3568
3673
  let shareSet;
3569
3674
  let evmAddress;
@@ -3595,7 +3700,7 @@ async function createDecaneConnect(config) {
3595
3700
  version: 1
3596
3701
  });
3597
3702
  const serverIv = generateRandomBytes(12);
3598
- await api.putShare(jwt, {
3703
+ await withDeadline(api.putShare(jwt, {
3599
3704
  ciphertext: shareSet.server,
3600
3705
  iv: serverIv,
3601
3706
  wrappedShareMeta: {
@@ -3605,7 +3710,7 @@ async function createDecaneConnect(config) {
3605
3710
  evmAddress,
3606
3711
  solanaAddress,
3607
3712
  tronAddress
3608
- });
3713
+ }), remainingBudget(), "storing your wallet");
3609
3714
  zeroize(shareSet.server);
3610
3715
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
3611
3716
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
@@ -3621,7 +3726,11 @@ async function createDecaneConnect(config) {
3621
3726
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
3622
3727
  }
3623
3728
  }
3624
- const confirmed = await session.start(shareSet.device, jwt, addresses);
3729
+ const confirmed = await withDeadline(
3730
+ session.start(shareSet.device, jwt, addresses),
3731
+ remainingBudget(),
3732
+ "opening the signing session"
3733
+ );
3625
3734
  zeroize(shareSet.device);
3626
3735
  if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3627
3736
  console.warn(
@@ -3632,42 +3741,49 @@ async function createDecaneConnect(config) {
3632
3741
  addresses.solana = solanaAddress = confirmed.solana;
3633
3742
  addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
3634
3743
  if (serverAssistedRecovery) {
3635
- for (let attempt = 1; attempt <= 2; attempt++) {
3636
- try {
3637
- const tee = new TeeClient(teeUrl);
3638
- await tee.enrolAuthShare(jwt, shareSet.recovery);
3639
- break;
3640
- } catch (e) {
3641
- if (attempt === 2) {
3642
- console.warn(
3643
- "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.",
3644
- e
3645
- );
3646
- }
3647
- }
3648
- }
3744
+ void enrolInBackground(jwt, new Uint8Array(shareSet.recovery));
3649
3745
  }
3650
- if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
3651
- try {
3652
- const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3653
- if (choice.wants) {
3654
- const file = await buildRecoveryShareFile(
3655
- shareSet.recovery,
3656
- choice.password,
3657
- addresses,
3658
- shareSet.version,
3659
- choice.passwordHint
3660
- );
3661
- await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
3662
- await api.markRecoveryShareSaved(jwt);
3663
- }
3664
- } catch (e) {
3665
- console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
3666
- }
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
+ );
3667
3755
  }
3668
3756
  zeroize(shareSet.recovery);
3669
3757
  return addresses;
3670
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
+ }
3671
3787
  async function returningUserUnlock(userId, jwt) {
3672
3788
  const rawAddresses = await api.getAddresses(jwt);
3673
3789
  const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
@@ -3753,6 +3869,7 @@ async function createDecaneConnect(config) {
3753
3869
  }
3754
3870
  throw e;
3755
3871
  }
3872
+ void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
3756
3873
  zeroize(deviceShare);
3757
3874
  if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3758
3875
  console.warn(
@@ -7204,6 +7321,7 @@ export {
7204
7321
  NewDeviceError,
7205
7322
  OriginNotAllowedError,
7206
7323
  PromptPinNotConfiguredError,
7324
+ ProvisioningTimeoutError,
7207
7325
  ReconnectRequiredError,
7208
7326
  RecoveryFailedError,
7209
7327
  RecoveryWalletMismatchError,