decane-connect-kit 1.0.3 → 2.0.1

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.js CHANGED
@@ -1,3 +1,155 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/social/errors.ts
12
+ var errors_exports = {};
13
+ __export(errors_exports, {
14
+ AttestationError: () => AttestationError,
15
+ CallSequenceError: () => CallSequenceError,
16
+ DecaneConnectError: () => DecaneConnectError,
17
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
18
+ InvalidApiKeyError: () => InvalidApiKeyError,
19
+ InvalidPinError: () => InvalidPinError,
20
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
21
+ NetworkError: () => NetworkError,
22
+ NewDeviceError: () => NewDeviceError,
23
+ OriginNotAllowedError: () => OriginNotAllowedError,
24
+ PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
25
+ ReconnectRequiredError: () => ReconnectRequiredError,
26
+ RecoveryFailedError: () => RecoveryFailedError,
27
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
28
+ SessionExpiredError: () => SessionExpiredError,
29
+ ShareCorruptedError: () => ShareCorruptedError,
30
+ UnlockRequiredError: () => UnlockRequiredError,
31
+ UserCancelledError: () => UserCancelledError,
32
+ WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
33
+ WrongPasswordError: () => WrongPasswordError
34
+ });
35
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
36
+ var init_errors = __esm({
37
+ "src/social/errors.ts"() {
38
+ "use strict";
39
+ DecaneConnectError = class extends Error {
40
+ constructor(code, message, retryable = false) {
41
+ super(message);
42
+ this.name = "DecaneConnectError";
43
+ this.code = code;
44
+ this.retryable = retryable;
45
+ }
46
+ };
47
+ InvalidApiKeyError = class extends DecaneConnectError {
48
+ constructor() {
49
+ super("INVALID_API_KEY", "API key is invalid or revoked");
50
+ }
51
+ };
52
+ OriginNotAllowedError = class extends DecaneConnectError {
53
+ constructor(origin) {
54
+ super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
55
+ }
56
+ };
57
+ SessionExpiredError = class extends DecaneConnectError {
58
+ constructor() {
59
+ super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
60
+ }
61
+ };
62
+ UnlockRequiredError = class extends DecaneConnectError {
63
+ constructor() {
64
+ super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
65
+ }
66
+ };
67
+ ReconnectRequiredError = class extends DecaneConnectError {
68
+ constructor() {
69
+ super(
70
+ "RECONNECT_REQUIRED",
71
+ "Your session ended \u2014 sign in again to unlock this wallet",
72
+ true
73
+ );
74
+ }
75
+ };
76
+ UserCancelledError = class extends DecaneConnectError {
77
+ constructor() {
78
+ super("USER_CANCELLED", "User cancelled the operation", true);
79
+ }
80
+ };
81
+ WebAuthnNotSupportedError = class extends DecaneConnectError {
82
+ constructor() {
83
+ super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
84
+ }
85
+ };
86
+ InvalidPinError = class extends DecaneConnectError {
87
+ constructor() {
88
+ super("INVALID_PIN", "Incorrect PIN", true);
89
+ }
90
+ };
91
+ CallSequenceError = class extends DecaneConnectError {
92
+ constructor(msg) {
93
+ super("CALL_SEQUENCE", msg);
94
+ }
95
+ };
96
+ NetworkError = class extends DecaneConnectError {
97
+ constructor() {
98
+ super("NETWORK", "Network request failed", true);
99
+ }
100
+ };
101
+ ShareCorruptedError = class extends DecaneConnectError {
102
+ constructor() {
103
+ super("SHARE_CORRUPTED", "Stored device share is corrupted");
104
+ }
105
+ };
106
+ NewDeviceError = class extends DecaneConnectError {
107
+ constructor() {
108
+ super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
109
+ }
110
+ };
111
+ PromptPinNotConfiguredError = class extends DecaneConnectError {
112
+ constructor() {
113
+ super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
114
+ }
115
+ };
116
+ AttestationError = class extends DecaneConnectError {
117
+ constructor(msg) {
118
+ super("ATTESTATION_FAILED", msg);
119
+ }
120
+ };
121
+ RecoveryFailedError = class extends DecaneConnectError {
122
+ constructor(reason) {
123
+ super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
124
+ }
125
+ };
126
+ InvalidRecoveryFileError = class extends DecaneConnectError {
127
+ constructor() {
128
+ super("INVALID_RECOVERY_FILE", "This does not look like a valid recovery file", false);
129
+ }
130
+ };
131
+ WrongPasswordError = class extends DecaneConnectError {
132
+ constructor() {
133
+ super("WRONG_PASSWORD", "Incorrect password for this recovery file", true);
134
+ }
135
+ };
136
+ RecoveryWalletMismatchError = class extends DecaneConnectError {
137
+ constructor() {
138
+ super(
139
+ "RECOVERY_WALLET_MISMATCH",
140
+ "This recovery file is for a different wallet than the account you are signed into",
141
+ false
142
+ );
143
+ }
144
+ };
145
+ ExportNotAuthorizedError = class extends DecaneConnectError {
146
+ constructor() {
147
+ super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
148
+ }
149
+ };
150
+ }
151
+ });
152
+
1
153
  // src/core/emitter.ts
2
154
  function createEmitter() {
3
155
  const listeners = /* @__PURE__ */ new Map();
@@ -1734,94 +1886,10 @@ function createDecane(config = {}) {
1734
1886
  }
1735
1887
 
1736
1888
  // src/social/client.ts
1889
+ init_errors();
1737
1890
  import { Wallet } from "ethers";
1738
1891
  import { Transaction as Transaction2, VersionedTransaction, Keypair } from "@solana/web3.js";
1739
1892
 
1740
- // src/social/errors.ts
1741
- var DecaneConnectError = class extends Error {
1742
- constructor(code, message, retryable = false) {
1743
- super(message);
1744
- this.name = "DecaneConnectError";
1745
- this.code = code;
1746
- this.retryable = retryable;
1747
- }
1748
- };
1749
- var InvalidApiKeyError = class extends DecaneConnectError {
1750
- constructor() {
1751
- super("INVALID_API_KEY", "API key is invalid or revoked");
1752
- }
1753
- };
1754
- var OriginNotAllowedError = class extends DecaneConnectError {
1755
- constructor(origin) {
1756
- super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
1757
- }
1758
- };
1759
- var SessionExpiredError = class extends DecaneConnectError {
1760
- constructor() {
1761
- super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
1762
- }
1763
- };
1764
- var UnlockRequiredError = class extends DecaneConnectError {
1765
- constructor() {
1766
- super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
1767
- }
1768
- };
1769
- var UserCancelledError = class extends DecaneConnectError {
1770
- constructor() {
1771
- super("USER_CANCELLED", "User cancelled the operation", true);
1772
- }
1773
- };
1774
- var WebAuthnNotSupportedError = class extends DecaneConnectError {
1775
- constructor() {
1776
- super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
1777
- }
1778
- };
1779
- var InvalidPinError = class extends DecaneConnectError {
1780
- constructor() {
1781
- super("INVALID_PIN", "Incorrect PIN", true);
1782
- }
1783
- };
1784
- var CallSequenceError = class extends DecaneConnectError {
1785
- constructor(msg) {
1786
- super("CALL_SEQUENCE", msg);
1787
- }
1788
- };
1789
- var NetworkError = class extends DecaneConnectError {
1790
- constructor() {
1791
- super("NETWORK", "Network request failed", true);
1792
- }
1793
- };
1794
- var ShareCorruptedError = class extends DecaneConnectError {
1795
- constructor() {
1796
- super("SHARE_CORRUPTED", "Stored device share is corrupted");
1797
- }
1798
- };
1799
- var NewDeviceError = class extends DecaneConnectError {
1800
- constructor() {
1801
- super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
1802
- }
1803
- };
1804
- var PromptPinNotConfiguredError = class extends DecaneConnectError {
1805
- constructor() {
1806
- super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
1807
- }
1808
- };
1809
- var AttestationError = class extends DecaneConnectError {
1810
- constructor(msg) {
1811
- super("ATTESTATION_FAILED", msg);
1812
- }
1813
- };
1814
- var RecoveryFailedError = class extends DecaneConnectError {
1815
- constructor(reason) {
1816
- super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
1817
- }
1818
- };
1819
- var InvalidPhraseError = class extends DecaneConnectError {
1820
- constructor() {
1821
- super("INVALID_PHRASE", "Recovery phrase is invalid or corrupted", true);
1822
- }
1823
- };
1824
-
1825
1893
  // src/social/events.ts
1826
1894
  var EventEmitter = class {
1827
1895
  constructor() {
@@ -1840,6 +1908,7 @@ var EventEmitter = class {
1840
1908
  };
1841
1909
 
1842
1910
  // src/social/api.ts
1911
+ init_errors();
1843
1912
  var ERROR_MAP = {
1844
1913
  INVALID_API_KEY: InvalidApiKeyError,
1845
1914
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
@@ -1910,13 +1979,26 @@ var ApiClient = class {
1910
1979
  })
1911
1980
  });
1912
1981
  }
1982
+ // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
1983
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
1984
+ return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
1985
+ headers: { Authorization: `Bearer ${jwt}` }
1986
+ });
1987
+ }
1988
+ async postWebAuthnRegister(jwt, response, origin) {
1989
+ await this._request("/webauthn/register", {
1990
+ method: "POST",
1991
+ headers: { Authorization: `Bearer ${jwt}` },
1992
+ body: JSON.stringify({ response, origin })
1993
+ });
1994
+ }
1913
1995
  async revoke(jwt) {
1914
1996
  await this._request("/auth/revoke", {
1915
1997
  method: "POST",
1916
1998
  headers: { Authorization: `Bearer ${jwt}` }
1917
1999
  });
1918
2000
  }
1919
- // ── Recovery (passkey-sync backup + phrase-saved flag) ────────────────────
2001
+ // ── Recovery (passkey-sync backup + recovery-share-saved flag) ────────────
1920
2002
  async hasRecoveryBackup(jwt) {
1921
2003
  const data = await this._request("/recovery/backup/exists", {
1922
2004
  headers: { Authorization: `Bearer ${jwt}` }
@@ -1957,8 +2039,19 @@ var ApiClient = class {
1957
2039
  headers: { Authorization: `Bearer ${jwt}` }
1958
2040
  });
1959
2041
  }
1960
- async markPhraseSaved(jwt) {
1961
- await this._request("/recovery/phrase-saved", {
2042
+ async markRecoveryShareSaved(jwt) {
2043
+ await this._request("/recovery/share-saved", {
2044
+ method: "POST",
2045
+ headers: { Authorization: `Bearer ${jwt}` }
2046
+ });
2047
+ }
2048
+ async getRecoveryStatus(jwt) {
2049
+ return this._request("/recovery/status", {
2050
+ headers: { Authorization: `Bearer ${jwt}` }
2051
+ });
2052
+ }
2053
+ async dismissRecoveryNag(jwt) {
2054
+ await this._request("/recovery/nag-dismissed", {
1962
2055
  method: "POST",
1963
2056
  headers: { Authorization: `Bearer ${jwt}` }
1964
2057
  });
@@ -1967,8 +2060,8 @@ var ApiClient = class {
1967
2060
  function bytesToB64(bytes) {
1968
2061
  return btoa(String.fromCharCode(...bytes));
1969
2062
  }
1970
- function b64ToBytes(b642) {
1971
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
2063
+ function b64ToBytes(b644) {
2064
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
1972
2065
  }
1973
2066
 
1974
2067
  // src/social/storage.ts
@@ -1994,7 +2087,11 @@ var Storage = class {
1994
2087
  }
1995
2088
  };
1996
2089
 
2090
+ // src/social/teeClient.ts
2091
+ import { startAuthentication } from "@simplewebauthn/browser";
2092
+
1997
2093
  // src/social/attestation.ts
2094
+ init_errors();
1998
2095
  import { getCollateralAndVerify } from "@phala/dcap-qvl";
1999
2096
 
2000
2097
  // ../../infra/tee-measurement.json
@@ -2053,11 +2150,15 @@ async function verifyTeeAttestation(teeUrl, nonce) {
2053
2150
  }
2054
2151
 
2055
2152
  // src/social/teeClient.ts
2153
+ init_errors();
2056
2154
  function bytesToBase64(b) {
2057
2155
  let s = "";
2058
2156
  for (const byte of b) s += String.fromCharCode(byte);
2059
2157
  return btoa(s);
2060
2158
  }
2159
+ function base64ToBytes(s) {
2160
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2161
+ }
2061
2162
  var TeeClient = class {
2062
2163
  constructor(teeUrl) {
2063
2164
  this.teeUrl = teeUrl;
@@ -2071,6 +2172,13 @@ var TeeClient = class {
2071
2172
  await verifyTeeAttestation(this.teeUrl, nonceHex);
2072
2173
  this.attested = true;
2073
2174
  }
2175
+ /**
2176
+ * Opens a signing session and returns the addresses the TEE derived from the
2177
+ * key it actually reconstructed — authoritative, unlike the backend's
2178
+ * recorded copy, which nothing else can verify. Returns null if the TEE
2179
+ * couldn't derive them (older signer build, or a transient share-fetch
2180
+ * failure); callers fall back to the backend's addresses in that case.
2181
+ */
2074
2182
  async openSession(deviceShare, jwt) {
2075
2183
  await this.ensureAttested();
2076
2184
  const res = await fetch(`${this.teeUrl}/signer/session`, {
@@ -2079,18 +2187,94 @@ var TeeClient = class {
2079
2187
  body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2080
2188
  });
2081
2189
  if (!res.ok) throw new Error("session open failed");
2082
- this.handle = (await res.json()).handle;
2190
+ const body = await res.json();
2191
+ this.handle = body.handle;
2192
+ return body.addresses ?? null;
2083
2193
  }
2084
- /** Verify a candidate device share (from a recovery phrase) without exposing shares. */
2085
- async verifyRecoveryCandidate(deviceShare, jwt) {
2194
+ /**
2195
+ * Flow B: exchange a recovery share (from a recovery-share file) for a
2196
+ * fresh device share. The server verifies the reconstructed master
2197
+ * actually matches this account's addresses before returning anything —
2198
+ * see the security note in the TEE's /signer/recover handler. Rotates the
2199
+ * whole share set: the caller MUST persist the returned recoveryShare as a
2200
+ * new recovery-share file, since the old one stops working immediately.
2201
+ */
2202
+ async recoverWithShare(jwt, recoveryShare) {
2086
2203
  await this.ensureAttested();
2087
- const res = await fetch(`${this.teeUrl}/signer/verify-recovery`, {
2204
+ const res = await fetch(`${this.teeUrl}/signer/recover`, {
2088
2205
  method: "POST",
2089
2206
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2090
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2207
+ body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
2208
+ });
2209
+ if (res.status === 403) {
2210
+ const { RecoveryWalletMismatchError: RecoveryWalletMismatchError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2211
+ throw new RecoveryWalletMismatchError2();
2212
+ }
2213
+ if (!res.ok) throw new Error("recovery failed");
2214
+ const body = await res.json();
2215
+ return {
2216
+ deviceShare: base64ToBytes(body.deviceShare),
2217
+ recoveryShare: base64ToBytes(body.recoveryShare),
2218
+ shareSetVersion: body.shareSetVersion
2219
+ };
2220
+ }
2221
+ /**
2222
+ * Request a fresh WebAuthn assertion challenge and complete it in the
2223
+ * browser via @simplewebauthn/browser's startAuthentication(), which
2224
+ * handles all the base64url (de)serialization the raw navigator.credentials
2225
+ * API requires. Shared by rotateShares() and exportPortableBackup() — both
2226
+ * require proof of fresh authenticator possession, not just an active session.
2227
+ */
2228
+ async completeAssertion(jwt) {
2229
+ const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2230
+ method: "POST",
2231
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2232
+ body: JSON.stringify({ rpId: window.location.hostname })
2091
2233
  });
2092
- if (!res.ok) return false;
2093
- return (await res.json()).verified === true;
2234
+ if (!res.ok) throw new ExportNotAuthorizedError();
2235
+ const { challengeId, options } = await res.json();
2236
+ let assertion;
2237
+ try {
2238
+ assertion = await startAuthentication({ optionsJSON: options });
2239
+ } catch {
2240
+ const { UserCancelledError: UserCancelledError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2241
+ throw new UserCancelledError2();
2242
+ }
2243
+ return { challengeId, assertion };
2244
+ }
2245
+ /** Voluntary rotation — e.g. after a suspected device compromise, or to invalidate an old recovery file. */
2246
+ async rotateShares(jwt) {
2247
+ if (!this.handle) throw new Error("no session");
2248
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2249
+ const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2250
+ method: "POST",
2251
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2252
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2253
+ });
2254
+ if (!res.ok) throw new ExportNotAuthorizedError();
2255
+ const body = await res.json();
2256
+ return {
2257
+ deviceShare: base64ToBytes(body.deviceShare),
2258
+ recoveryShare: base64ToBytes(body.recoveryShare),
2259
+ shareSetVersion: body.shareSetVersion
2260
+ };
2261
+ }
2262
+ /** The two-share bearer portable backup — see recovery/portableBackup.ts. Rotates the set on export. */
2263
+ async exportPortableBackup(jwt) {
2264
+ if (!this.handle) throw new Error("no session");
2265
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2266
+ const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2267
+ method: "POST",
2268
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2269
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2270
+ });
2271
+ if (!res.ok) throw new ExportNotAuthorizedError();
2272
+ const body = await res.json();
2273
+ return {
2274
+ deviceShare: base64ToBytes(body.deviceShare),
2275
+ recoveryShare: base64ToBytes(body.recoveryShare),
2276
+ shareSetVersion: body.shareSetVersion
2277
+ };
2094
2278
  }
2095
2279
  hasSession() {
2096
2280
  return this.handle !== null;
@@ -2123,6 +2307,7 @@ var TeeClient = class {
2123
2307
  };
2124
2308
 
2125
2309
  // src/social/session.ts
2310
+ init_errors();
2126
2311
  var SessionManager = class {
2127
2312
  constructor(events, durationMs, teeUrl) {
2128
2313
  this.events = events;
@@ -2137,9 +2322,24 @@ var SessionManager = class {
2137
2322
  window.addEventListener("pagehide", () => this.end());
2138
2323
  }
2139
2324
  }
2140
- async start(deviceShare, jwt, addresses) {
2325
+ /**
2326
+ * Opens the TEE session and resolves which addresses this wallet really has.
2327
+ *
2328
+ * `fallback` is whatever the backend recorded; the TEE's answer is derived
2329
+ * from the signing key itself and therefore outranks it. Returns the
2330
+ * resolved addresses so callers can propagate the corrected values rather
2331
+ * than continuing to use a stale record.
2332
+ */
2333
+ async start(deviceShare, jwt, fallback) {
2141
2334
  this.tee = new TeeClient(this.teeUrl);
2142
- await this.tee.openSession(deviceShare, jwt);
2335
+ const verified = await this.tee.openSession(deviceShare, jwt);
2336
+ const addresses = verified ?? fallback;
2337
+ if (!addresses) {
2338
+ this.tee = null;
2339
+ throw new Error(
2340
+ "No addresses available for this wallet \u2014 the signer could not derive them and none are recorded"
2341
+ );
2342
+ }
2143
2343
  this.evmAddress = addresses.evm;
2144
2344
  this.solanaAddress = addresses.solana;
2145
2345
  this.expiresAt = Date.now() + this.durationMs;
@@ -2149,6 +2349,7 @@ var SessionManager = class {
2149
2349
  this.events.emit("session-expired");
2150
2350
  }, this.durationMs);
2151
2351
  this.events.emit("session-started");
2352
+ return addresses;
2152
2353
  }
2153
2354
  end() {
2154
2355
  void this.tee?.closeSession();
@@ -2178,24 +2379,32 @@ var SessionManager = class {
2178
2379
  };
2179
2380
 
2180
2381
  // src/social/crypto/shares.ts
2181
- function xorBytes(a, b) {
2182
- if (a.length !== b.length) throw new Error("xorBytes: length mismatch");
2183
- const out = new Uint8Array(a.length);
2184
- for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
2185
- return out;
2186
- }
2382
+ import { secp256k1 } from "@noble/curves/secp256k1";
2383
+ import { split, combine } from "shamir-secret-sharing";
2187
2384
  function generateRandomBytes(n) {
2188
2385
  return crypto.getRandomValues(new Uint8Array(n));
2189
2386
  }
2190
- function generateWalletShares() {
2191
- const privateKey = generateRandomBytes(32);
2192
- const deviceShare = generateRandomBytes(32);
2193
- const serverShare = xorBytes(privateKey, deviceShare);
2194
- return { privateKey, deviceShare, serverShare };
2195
- }
2196
2387
  function zeroize(bytes) {
2197
2388
  bytes.fill(0);
2198
2389
  }
2390
+ function generateMaster() {
2391
+ let master;
2392
+ do {
2393
+ master = generateRandomBytes(32);
2394
+ } while (!secp256k1.utils.isValidPrivateKey(master));
2395
+ return master;
2396
+ }
2397
+ async function splitMaster(master, version = 1) {
2398
+ if (master.length !== 32) throw new Error("master must be 32 bytes");
2399
+ const parts = await split(master, 3, 2);
2400
+ if (parts.length !== 3) throw new Error("unexpected share count");
2401
+ return { device: parts[0], server: parts[1], recovery: parts[2], version };
2402
+ }
2403
+ async function combineShares(a, b) {
2404
+ const master = await combine([a, b]);
2405
+ if (master.length !== 32) throw new Error("reconstruction produced wrong length");
2406
+ return master;
2407
+ }
2199
2408
 
2200
2409
  // src/social/crypto/wrap.ts
2201
2410
  function buf(u) {
@@ -2240,6 +2449,8 @@ function validatePin(pin) {
2240
2449
  }
2241
2450
 
2242
2451
  // src/social/crypto/passkey.ts
2452
+ init_errors();
2453
+ import { startRegistration } from "@simplewebauthn/browser";
2243
2454
  function buf3(u) {
2244
2455
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2245
2456
  }
@@ -2247,30 +2458,24 @@ function isPasskeySupported() {
2247
2458
  return typeof window !== "undefined" && !!window.PublicKeyCredential && "isUserVerifyingPlatformAuthenticatorAvailable" in window.PublicKeyCredential;
2248
2459
  }
2249
2460
  async function registerPasskey(opts) {
2250
- const challenge = crypto.getRandomValues(new Uint8Array(32));
2251
- let cred;
2461
+ let response;
2252
2462
  try {
2253
- cred = await navigator.credentials.create({
2254
- publicKey: {
2255
- challenge: buf3(challenge),
2256
- rp: { id: opts.rpId, name: opts.rpName },
2257
- user: { id: buf3(opts.userId), name: opts.userName, displayName: opts.userName },
2258
- pubKeyCredParams: [
2259
- { type: "public-key", alg: -7 },
2260
- { type: "public-key", alg: -257 }
2261
- ],
2262
- authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
2263
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
2264
- }
2265
- });
2463
+ response = await startRegistration({ optionsJSON: opts.optionsJSON });
2266
2464
  } catch (e) {
2267
2465
  if (e.name === "NotAllowedError") throw new UserCancelledError();
2268
2466
  throw e;
2269
2467
  }
2270
- const ext = cred.getClientExtensionResults();
2271
- const prfResults = ext?.prf?.results;
2272
- const prfOutput = prfResults?.first ? new Uint8Array(prfResults.first) : void 0;
2273
- return { credentialId: new Uint8Array(cred.rawId), prfOutput };
2468
+ const extResults = response.clientExtensionResults;
2469
+ const prfResults = extResults?.prf?.results;
2470
+ let prfOutput;
2471
+ if (prfResults?.first) {
2472
+ prfOutput = typeof prfResults.first === "string" ? Uint8Array.from(atob(prfResults.first.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)) : new Uint8Array(prfResults.first);
2473
+ }
2474
+ const credentialId = Uint8Array.from(
2475
+ atob(response.id.replace(/-/g, "+").replace(/_/g, "/")),
2476
+ (c) => c.charCodeAt(0)
2477
+ );
2478
+ return { credentialId, prfOutput, response };
2274
2479
  }
2275
2480
  async function getPrfOutput(opts) {
2276
2481
  const challenge = crypto.getRandomValues(new Uint8Array(32));
@@ -2295,6 +2500,28 @@ async function getPrfOutput(opts) {
2295
2500
  return new Uint8Array(prfResults.first);
2296
2501
  }
2297
2502
 
2503
+ // src/social/webauthnRegistration.ts
2504
+ function bufFromBytes(u) {
2505
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2506
+ }
2507
+ async function registerWebAuthnCredential(opts) {
2508
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2509
+ const optionsJSON = {
2510
+ ...baseOptions,
2511
+ extensions: {
2512
+ ...baseOptions.extensions,
2513
+ prf: { eval: { first: bufFromBytes(opts.prfSalt) } }
2514
+ }
2515
+ };
2516
+ const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2517
+ try {
2518
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2519
+ } catch (e) {
2520
+ console.warn("decane-connect-kit: WebAuthn server-side registration failed; PRF-based unlock still works, but rotateShares()/exportPortableBackup() will not until this is retried.", e);
2521
+ }
2522
+ return { credentialId, prfOutput };
2523
+ }
2524
+
2298
2525
  // src/social/auth/email.ts
2299
2526
  async function startEmailAuth(api, email) {
2300
2527
  await api.authEmailStart(email);
@@ -2341,28 +2568,102 @@ async function fetchWrappedShareBackup(api, jwt) {
2341
2568
  return api.getRecoveryBackup(jwt);
2342
2569
  }
2343
2570
 
2344
- // src/social/recovery/phrase.ts
2345
- import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic } from "@scure/bip39";
2346
- import { wordlist } from "@scure/bip39/wordlists/english.js";
2347
- function shareToPhrase(deviceShare) {
2348
- if (deviceShare.length !== 32) throw new Error("deviceShare must be 32 bytes");
2349
- return entropyToMnemonic(deviceShare, wordlist);
2571
+ // src/social/recovery/recoveryShareFile.ts
2572
+ init_errors();
2573
+ var PBKDF2_ITERATIONS2 = 6e5;
2574
+ var FILE_VERSION = 2;
2575
+ function b642(u) {
2576
+ let s = "";
2577
+ for (const b of u) s += String.fromCharCode(b);
2578
+ return btoa(s);
2350
2579
  }
2351
- function phraseToShare(phrase) {
2352
- const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
2353
- if (!validateMnemonic(normalized, wordlist)) throw new InvalidPhraseError();
2354
- const entropy = mnemonicToEntropy(normalized, wordlist);
2355
- if (entropy.length !== 32) throw new InvalidPhraseError();
2356
- return entropy;
2580
+ function unb642(s) {
2581
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2357
2582
  }
2358
- function phraseConfirmationChallenge(phrase) {
2359
- const words = phrase.trim().split(/\s+/);
2360
- const chosen = /* @__PURE__ */ new Set();
2361
- while (chosen.size < 3) chosen.add(Math.floor(Math.random() * words.length));
2362
- return [...chosen].sort((a, b) => a - b).map((i) => ({ position: i + 1, expected: words[i] }));
2583
+ function buf4(u) {
2584
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2585
+ }
2586
+ async function deriveFileKey(password, salt, iterations) {
2587
+ const base = await crypto.subtle.importKey(
2588
+ "raw",
2589
+ new TextEncoder().encode(password),
2590
+ "PBKDF2",
2591
+ false,
2592
+ ["deriveKey"]
2593
+ );
2594
+ return crypto.subtle.deriveKey(
2595
+ { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
2596
+ base,
2597
+ { name: "AES-GCM", length: 256 },
2598
+ false,
2599
+ ["encrypt", "decrypt"]
2600
+ );
2601
+ }
2602
+ async function buildRecoveryShareFile(share, password, addresses, shareSetVersion, passwordHint) {
2603
+ if (share.length !== 33) throw new Error("share must be 33 bytes");
2604
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2605
+ if (passwordHint && passwordHint.trim() === password) {
2606
+ throw new Error("password hint must not be the password");
2607
+ }
2608
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2609
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2610
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
2611
+ const ct = new Uint8Array(
2612
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
2613
+ );
2614
+ return {
2615
+ version: FILE_VERSION,
2616
+ type: "decane-recovery-share",
2617
+ shareSetVersion,
2618
+ kdf: "pbkdf2-sha256",
2619
+ kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
2620
+ cipher: "aes-256-gcm",
2621
+ iv: b642(iv),
2622
+ ciphertext: b642(ct),
2623
+ addresses,
2624
+ passwordHint: passwordHint?.trim() || null,
2625
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2626
+ };
2627
+ }
2628
+ function parseRecoveryShareFile(json) {
2629
+ const raw = json;
2630
+ if (raw?.type === "decane-recovery") {
2631
+ throw new InvalidRecoveryFileError();
2632
+ }
2633
+ const f = json;
2634
+ if (!f || f.type !== "decane-recovery-share" || f.version !== FILE_VERSION || f.kdf !== "pbkdf2-sha256" || f.cipher !== "aes-256-gcm" || typeof f.iv !== "string" || typeof f.ciphertext !== "string" || typeof f.kdfParams?.salt !== "string" || typeof f.kdfParams?.iterations !== "number" || typeof f.shareSetVersion !== "number" || typeof f.addresses?.evm !== "string" || typeof f.addresses?.solana !== "string") {
2635
+ throw new InvalidRecoveryFileError();
2636
+ }
2637
+ if (f.kdfParams.iterations < 1e5 || f.kdfParams.iterations > 5e6) {
2638
+ throw new InvalidRecoveryFileError();
2639
+ }
2640
+ return f;
2641
+ }
2642
+ async function decryptRecoveryShareFile(file, password) {
2643
+ const salt = unb642(file.kdfParams.salt);
2644
+ const iv = unb642(file.iv);
2645
+ const ct = unb642(file.ciphertext);
2646
+ const key = await deriveFileKey(password, salt, file.kdfParams.iterations);
2647
+ let share;
2648
+ try {
2649
+ share = new Uint8Array(
2650
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
2651
+ );
2652
+ } catch {
2653
+ throw new WrongPasswordError();
2654
+ }
2655
+ if (share.length !== 33) {
2656
+ share.fill(0);
2657
+ throw new InvalidRecoveryFileError();
2658
+ }
2659
+ return share;
2660
+ }
2661
+ function recoveryShareFileName(evmAddress) {
2662
+ return `decane-recovery-${evmAddress.slice(0, 8)}.json`;
2363
2663
  }
2364
2664
 
2365
2665
  // src/social/recovery/recoveryFlow.ts
2666
+ init_errors();
2366
2667
  async function recoverDeviceShare(input) {
2367
2668
  const backup = await fetchWrappedShareBackup(input.api, input.jwt);
2368
2669
  if (backup) {
@@ -2396,37 +2697,87 @@ async function recoverDeviceShare(input) {
2396
2697
  });
2397
2698
  }
2398
2699
  }
2399
- if (!input.promptForPhrase) throw new NewDeviceError();
2400
- input.onProgress?.({ type: "phrase-prompt" });
2401
- const phrase = await input.promptForPhrase();
2402
- if (!phrase) throw new NewDeviceError();
2403
- let candidate;
2404
- try {
2405
- candidate = phraseToShare(phrase);
2406
- } catch {
2407
- throw new InvalidPhraseError();
2408
- }
2409
- input.onProgress?.({ type: "phrase-verifying" });
2700
+ if (!input.promptForRecoveryFile) throw new NewDeviceError();
2701
+ input.onProgress?.({ type: "recovery-file-prompt" });
2702
+ const supplied = await input.promptForRecoveryFile();
2703
+ if (!supplied) throw new NewDeviceError();
2704
+ const file = parseRecoveryShareFile(supplied.value);
2705
+ input.onProgress?.({ type: "password-prompt", passwordHint: file.passwordHint });
2706
+ const password = await supplied.getPassword();
2707
+ if (!password) throw new NewDeviceError();
2708
+ const recoveryShare = await decryptRecoveryShareFile(file, password);
2709
+ input.onProgress?.({ type: "recovery-verifying" });
2410
2710
  const tee = new TeeClient(input.teeUrl);
2411
- const verified = await tee.verifyRecoveryCandidate(candidate, input.jwt);
2412
- if (!verified) {
2413
- zeroize(candidate);
2414
- throw new RecoveryFailedError("phrase-mismatch");
2711
+ let result;
2712
+ try {
2713
+ result = await tee.recoverWithShare(input.jwt, recoveryShare);
2714
+ } finally {
2715
+ zeroize(recoveryShare);
2415
2716
  }
2416
- return candidate;
2717
+ input.onProgress?.({ type: "rotated", shareSetVersion: result.shareSetVersion });
2718
+ await input.onNewRecoveryShare?.(result.recoveryShare, result.shareSetVersion);
2719
+ zeroize(result.recoveryShare);
2720
+ return result.deviceShare;
2417
2721
  }
2418
2722
 
2419
- // src/social/recovery/phraseUi.ts
2420
- function buildPhraseResult(deviceShare) {
2421
- const phrase = shareToPhrase(deviceShare);
2422
- return { phrase, challenge: phraseConfirmationChallenge(phrase) };
2723
+ // src/social/recovery/portableBackup.ts
2724
+ init_errors();
2725
+ var PBKDF2_ITERATIONS3 = 6e5;
2726
+ var FILE_VERSION2 = 1;
2727
+ function b643(u) {
2728
+ let s = "";
2729
+ for (const b of u) s += String.fromCharCode(b);
2730
+ return btoa(s);
2423
2731
  }
2424
- function verifyConfirmationChallenge(challenge, userAnswers) {
2425
- if (userAnswers.length !== challenge.length) return false;
2426
- return challenge.every(({ position, expected }) => {
2427
- const answer = userAnswers.find((a) => a.position === position);
2428
- return !!answer && answer.word.trim().toLowerCase() === expected.toLowerCase();
2429
- });
2732
+ function buf5(u) {
2733
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2734
+ }
2735
+ async function deriveFileKey2(password, salt, iterations) {
2736
+ const base = await crypto.subtle.importKey(
2737
+ "raw",
2738
+ new TextEncoder().encode(password),
2739
+ "PBKDF2",
2740
+ false,
2741
+ ["deriveKey"]
2742
+ );
2743
+ return crypto.subtle.deriveKey(
2744
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
2745
+ base,
2746
+ { name: "AES-GCM", length: 256 },
2747
+ false,
2748
+ ["encrypt", "decrypt"]
2749
+ );
2750
+ }
2751
+ async function buildPortableBackup(deviceShare, recoveryShare, password, addresses, shareSetVersion, passwordHint) {
2752
+ if (deviceShare.length !== 33 || recoveryShare.length !== 33) {
2753
+ throw new Error("shares must be 33 bytes");
2754
+ }
2755
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2756
+ if (passwordHint && passwordHint.trim() === password) {
2757
+ throw new Error("password hint must not be the password");
2758
+ }
2759
+ const plaintext = new TextEncoder().encode(
2760
+ JSON.stringify({ deviceShare: b643(deviceShare), recoveryShare: b643(recoveryShare) })
2761
+ );
2762
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2763
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2764
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
2765
+ const ct = new Uint8Array(
2766
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
2767
+ );
2768
+ return {
2769
+ version: FILE_VERSION2,
2770
+ type: "decane-portable-backup",
2771
+ shareSetVersion,
2772
+ kdf: "pbkdf2-sha256",
2773
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
2774
+ cipher: "aes-256-gcm",
2775
+ iv: b643(iv),
2776
+ ciphertext: b643(ct),
2777
+ addresses,
2778
+ passwordHint: passwordHint?.trim() || null,
2779
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2780
+ };
2430
2781
  }
2431
2782
 
2432
2783
  // src/social/client.ts
@@ -2498,6 +2849,18 @@ async function createDecaneConnect(config) {
2498
2849
  }
2499
2850
  };
2500
2851
  }
2852
+ async function persistRotatedDeviceShare(userId, deviceShare) {
2853
+ const stored = await storage.getShare(userId);
2854
+ if (!stored) {
2855
+ throw new CallSequenceError(
2856
+ "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
2857
+ );
2858
+ }
2859
+ const wrappingKey = await getWrappingKey(stored);
2860
+ const iv = generateRandomBytes(12);
2861
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
2862
+ await storage.putShare({ ...stored, wrappedDeviceShare, iv });
2863
+ }
2501
2864
  async function getWrappingKey(stored) {
2502
2865
  if (stored.unlockMethod === "passkey") {
2503
2866
  const prfOutput = await getPrfOutput({
@@ -2517,15 +2880,7 @@ async function createDecaneConnect(config) {
2517
2880
  let credentialId;
2518
2881
  let prfOutput;
2519
2882
  try {
2520
- const authType = config.authMethods[0] ?? "social";
2521
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2522
- const result = await registerPasskey({
2523
- rpId,
2524
- rpName,
2525
- userId: new TextEncoder().encode(userId),
2526
- userName: `decane-wallet-${authType}-${dateStr}`,
2527
- prfSalt
2528
- });
2883
+ const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2529
2884
  credentialId = result.credentialId;
2530
2885
  prfOutput = result.prfOutput;
2531
2886
  } catch (e) {
@@ -2564,15 +2919,7 @@ async function createDecaneConnect(config) {
2564
2919
  if (isPasskeySupported()) {
2565
2920
  const prfSalt = generateRandomBytes(32);
2566
2921
  try {
2567
- const authType = config.authMethods[0] ?? "social";
2568
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2569
- const { credentialId, prfOutput } = await registerPasskey({
2570
- rpId,
2571
- rpName,
2572
- userId: new TextEncoder().encode(userId),
2573
- userName: `decane-wallet-${authType}-${dateStr}`,
2574
- prfSalt
2575
- });
2922
+ const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2576
2923
  if (prfOutput) {
2577
2924
  unlockMethod = "passkey";
2578
2925
  wrappingKey = await importAesKey(prfOutput);
@@ -2595,19 +2942,21 @@ async function createDecaneConnect(config) {
2595
2942
  ({ wrappingKey, meta } = await pinSetup());
2596
2943
  unlockMethod = "pin";
2597
2944
  }
2598
- const { privateKey, deviceShare, serverShare } = generateWalletShares();
2945
+ const master = generateMaster();
2946
+ let shareSet;
2599
2947
  let evmAddress;
2600
2948
  let solanaAddress;
2601
2949
  try {
2602
- evmAddress = new Wallet(toHex(privateKey)).address;
2603
- solanaAddress = Keypair.fromSeed(privateKey).publicKey.toBase58();
2950
+ evmAddress = new Wallet(toHex(master)).address;
2951
+ solanaAddress = Keypair.fromSeed(master).publicKey.toBase58();
2952
+ shareSet = await splitMaster(master, 1);
2604
2953
  } finally {
2605
- zeroize(privateKey);
2954
+ zeroize(master);
2606
2955
  }
2607
2956
  const iv = generateRandomBytes(12);
2608
2957
  const wrappedDeviceShare = await aesGcmEncrypt(
2609
2958
  wrappingKey,
2610
- deviceShare,
2959
+ shareSet.device,
2611
2960
  iv
2612
2961
  );
2613
2962
  await storage.putShare({
@@ -2623,7 +2972,7 @@ async function createDecaneConnect(config) {
2623
2972
  });
2624
2973
  const serverIv = generateRandomBytes(12);
2625
2974
  await api.putShare(jwt, {
2626
- ciphertext: serverShare,
2975
+ ciphertext: shareSet.server,
2627
2976
  iv: serverIv,
2628
2977
  wrappedShareMeta: {
2629
2978
  unlockMethod
@@ -2632,7 +2981,7 @@ async function createDecaneConnect(config) {
2632
2981
  evmAddress,
2633
2982
  solanaAddress
2634
2983
  });
2635
- zeroize(serverShare);
2984
+ zeroize(shareSet.server);
2636
2985
  const addresses = { evm: evmAddress, solana: solanaAddress };
2637
2986
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
2638
2987
  try {
@@ -2647,32 +2996,47 @@ async function createDecaneConnect(config) {
2647
2996
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
2648
2997
  }
2649
2998
  }
2650
- await session.start(deviceShare, jwt, addresses);
2651
- if (config.offerPhraseAtSignup !== false && config.onPhraseGenerated) {
2999
+ const confirmed = await session.start(shareSet.device, jwt, addresses);
3000
+ zeroize(shareSet.device);
3001
+ if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3002
+ console.warn(
3003
+ `decane-connect-kit: signer derived a different EVM address (${confirmed.evm}) than this device did (${addresses.evm}) during setup. Using the signer's.`
3004
+ );
3005
+ }
3006
+ addresses.evm = evmAddress = confirmed.evm;
3007
+ addresses.solana = solanaAddress = confirmed.solana;
3008
+ if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
2652
3009
  try {
2653
- const result = buildPhraseResult(deviceShare);
2654
- const { userConfirmed, userAnswers } = await config.onPhraseGenerated(result);
2655
- if (userConfirmed && verifyConfirmationChallenge(result.challenge, userAnswers)) {
2656
- await api.markPhraseSaved(jwt);
3010
+ const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3011
+ if (choice.wants) {
3012
+ const file = await buildRecoveryShareFile(
3013
+ shareSet.recovery,
3014
+ choice.password,
3015
+ addresses,
3016
+ shareSet.version,
3017
+ choice.passwordHint
3018
+ );
3019
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
3020
+ await api.markRecoveryShareSaved(jwt);
2657
3021
  }
2658
3022
  } catch (e) {
2659
- console.warn("decane-connect-kit: recovery phrase offer failed or was skipped.", e);
3023
+ console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
2660
3024
  }
2661
3025
  }
2662
- zeroize(deviceShare);
3026
+ zeroize(shareSet.recovery);
2663
3027
  return addresses;
2664
3028
  }
2665
3029
  async function returningUserUnlock(userId, jwt) {
2666
3030
  const rawAddresses = await api.getAddresses(jwt);
2667
- if (!rawAddresses.evm || !rawAddresses.solana) {
2668
- throw new CallSequenceError(
2669
- "No addresses are recorded for this user \u2014 inconsistent server state"
2670
- );
2671
- }
2672
- const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
3031
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
2673
3032
  const stored = await storage.getShare(userId);
2674
3033
  let deviceShare;
2675
3034
  if (!stored) {
3035
+ if (!config.onRecoveryRotated) {
3036
+ throw new CallSequenceError(
3037
+ "Recovery rotates the share set and issues a new recovery file, but no onRecoveryRotated callback is configured to save it. Configure onRecoveryRotated before allowing recovery."
3038
+ );
3039
+ }
2676
3040
  deviceShare = await recoverDeviceShare({
2677
3041
  userId,
2678
3042
  jwt,
@@ -2681,7 +3045,15 @@ async function createDecaneConnect(config) {
2681
3045
  api,
2682
3046
  storage,
2683
3047
  onProgress: config.onRecoveryProgress,
2684
- promptForPhrase: config.promptForRecoveryPhrase
3048
+ promptForRecoveryFile: config.promptForRecoveryFile,
3049
+ onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
3050
+ if (!config.onRecoveryRotated) return;
3051
+ if (!recordedAddresses) return;
3052
+ const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: recordedAddresses.evm, solanaAddress: recordedAddresses.solana });
3053
+ const file = await buildRecoveryShareFile(recoveryShare, password, recordedAddresses, shareSetVersion, passwordHint);
3054
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(recordedAddresses.evm));
3055
+ await api.markRecoveryShareSaved(jwt);
3056
+ }
2685
3057
  });
2686
3058
  if (!await storage.getShare(userId) && isPasskeySupported()) {
2687
3059
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
@@ -2698,8 +3070,13 @@ async function createDecaneConnect(config) {
2698
3070
  throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
2699
3071
  }
2700
3072
  }
2701
- await session.start(deviceShare, jwt, addresses);
3073
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
2702
3074
  zeroize(deviceShare);
3075
+ if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3076
+ console.warn(
3077
+ `decane-connect-kit: recorded EVM address (${recordedAddresses.evm}) does not match the signing key (${addresses.evm}). Using the signing key's address; the record has been corrected.`
3078
+ );
3079
+ }
2703
3080
  return addresses;
2704
3081
  }
2705
3082
  async function handleAuthResult(result) {
@@ -2761,14 +3138,24 @@ async function createDecaneConnect(config) {
2761
3138
  return handleAuthResult(result);
2762
3139
  },
2763
3140
  async unlock() {
2764
- if (!auth)
2765
- throw new CallSequenceError("Not connected \u2014 call connect() first");
3141
+ if (!auth) {
3142
+ throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
3143
+ }
2766
3144
  if (session.isActive()) return;
2767
3145
  await returningUserUnlock(auth.userId, auth.jwt);
2768
3146
  },
2769
3147
  isUnlocked() {
2770
3148
  return session.isActive();
2771
3149
  },
3150
+ /**
3151
+ * True when we have a persisted identity but no live auth state, i.e.
3152
+ * unlock() cannot succeed and the user must sign in again. Exposed on the
3153
+ * client (not just the React context) so non-React consumers can branch on
3154
+ * it without having to catch ReconnectRequiredError.
3155
+ */
3156
+ needsReconnect() {
3157
+ return !auth && !!persisted;
3158
+ },
2772
3159
  sessionExpiresAt() {
2773
3160
  return session.expiresAtTimestamp();
2774
3161
  },
@@ -2856,23 +3243,86 @@ async function createDecaneConnect(config) {
2856
3243
  getAddresses() {
2857
3244
  return auth?.addresses ?? persisted?.addresses ?? null;
2858
3245
  },
2859
- async generateRecoveryPhrase() {
3246
+ async rotateShares(opts) {
2860
3247
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2861
- const stored = await storage.getShare(auth.userId);
2862
- if (!stored) throw new NewDeviceError();
2863
- const wrappingKey = await getWrappingKey(stored);
2864
- const deviceShare = await aesGcmDecrypt(wrappingKey, stored.wrappedDeviceShare, stored.iv);
3248
+ const tee = session.requireActive();
3249
+ const result = await tee.rotateShares(auth.jwt);
2865
3250
  try {
2866
- return buildPhraseResult(deviceShare);
3251
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3252
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3253
+ const file = await buildRecoveryShareFile(
3254
+ result.recoveryShare,
3255
+ opts.password,
3256
+ addresses,
3257
+ result.shareSetVersion,
3258
+ opts.passwordHint
3259
+ );
3260
+ await api.markRecoveryShareSaved(auth.jwt);
3261
+ return file;
2867
3262
  } finally {
2868
- zeroize(deviceShare);
3263
+ zeroize(result.deviceShare);
3264
+ zeroize(result.recoveryShare);
2869
3265
  }
2870
3266
  },
2871
- async markPhraseSaved(challenge, userAnswers) {
3267
+ async exportPortableBackup(opts) {
2872
3268
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2873
- if (!verifyConfirmationChallenge(challenge, userAnswers)) return false;
2874
- await api.markPhraseSaved(auth.jwt);
2875
- return true;
3269
+ const tee = session.requireActive();
3270
+ const result = await tee.exportPortableBackup(auth.jwt);
3271
+ try {
3272
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3273
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3274
+ return await buildPortableBackup(
3275
+ result.deviceShare,
3276
+ result.recoveryShare,
3277
+ opts.password,
3278
+ addresses,
3279
+ result.shareSetVersion,
3280
+ opts.passwordHint
3281
+ );
3282
+ } finally {
3283
+ zeroize(result.deviceShare);
3284
+ zeroize(result.recoveryShare);
3285
+ }
3286
+ },
3287
+ async hasRecoveryShare() {
3288
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3289
+ const status = await api.getRecoveryStatus(auth.jwt);
3290
+ return status.hasRecoveryShare;
3291
+ },
3292
+ async shouldPromptRecovery() {
3293
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3294
+ const status = await api.getRecoveryStatus(auth.jwt);
3295
+ if (status.hasRecoveryShare || status.nagged) return false;
3296
+ try {
3297
+ const checks = [];
3298
+ if (auth.addresses.evm) {
3299
+ const { JsonRpcProvider } = await import("ethers");
3300
+ for (const chain of config.chains.filter((c) => c.startsWith("evm:"))) {
3301
+ const rpcUrl = config.rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
3302
+ if (!rpcUrl) continue;
3303
+ checks.push(
3304
+ new JsonRpcProvider(rpcUrl).getBalance(auth.addresses.evm).then((b) => b > 0n).catch(() => false)
3305
+ );
3306
+ }
3307
+ }
3308
+ if (auth.addresses.solana && config.chains.some((c) => c.startsWith("solana:"))) {
3309
+ const { Connection, PublicKey } = await import("@solana/web3.js");
3310
+ const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3311
+ const cluster = solChain.slice("solana:".length);
3312
+ const endpoint = cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
3313
+ checks.push(
3314
+ new Connection(endpoint).getBalance(new PublicKey(auth.addresses.solana)).then((b) => b > 0).catch(() => false)
3315
+ );
3316
+ }
3317
+ const results = await Promise.all(checks);
3318
+ return results.some(Boolean);
3319
+ } catch {
3320
+ return false;
3321
+ }
3322
+ },
3323
+ async dismissRecoveryPrompt() {
3324
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3325
+ await api.dismissRecoveryNag(auth.jwt);
2876
3326
  },
2877
3327
  async hasBackup() {
2878
3328
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
@@ -2887,16 +3337,30 @@ async function createDecaneConnect(config) {
2887
3337
  };
2888
3338
  return client;
2889
3339
  }
2890
- function bufferToB64u(buf4) {
2891
- return btoa(String.fromCharCode(...buf4)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3340
+ function bufferToB64u(buf6) {
3341
+ return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2892
3342
  }
2893
3343
  function b64uToBuffer(b64u) {
2894
- const b642 = b64u.replace(/-/g, "+").replace(/_/g, "/");
2895
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
3344
+ const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3345
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2896
3346
  }
2897
3347
  function toHex(bytes) {
2898
3348
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2899
3349
  }
3350
+ async function deriveAddressesFromShares(a, b) {
3351
+ const master = await combineShares(a, b);
3352
+ try {
3353
+ return {
3354
+ evm: new Wallet(toHex(master)).address,
3355
+ solana: Keypair.fromSeed(master).publicKey.toBase58()
3356
+ };
3357
+ } finally {
3358
+ zeroize(master);
3359
+ }
3360
+ }
3361
+
3362
+ // src/social/index.ts
3363
+ init_errors();
2900
3364
 
2901
3365
  // src/ui/WalletSelector.tsx
2902
3366
  import React2, { useEffect, useCallback, useState } from "react";
@@ -3819,6 +4283,15 @@ function countdown(expiresAt) {
3819
4283
  const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
3820
4284
  return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
3821
4285
  }
4286
+ function downloadJson(data, filename) {
4287
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
4288
+ const url = URL.createObjectURL(blob);
4289
+ const a = document.createElement("a");
4290
+ a.href = url;
4291
+ a.download = filename;
4292
+ a.click();
4293
+ URL.revokeObjectURL(url);
4294
+ }
3822
4295
  function CopyButton({ value }) {
3823
4296
  const [copied, setCopied] = useState2(false);
3824
4297
  async function copy() {
@@ -3846,7 +4319,54 @@ function CopyButton({ value }) {
3846
4319
  copied ? "\u2713" : "copy"
3847
4320
  );
3848
4321
  }
3849
- function AddressRow({ label, address }) {
4322
+ function useBalance(kind, address, chain, rpcUrls) {
4323
+ const [state, setState] = useState2({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
4324
+ useEffect2(() => {
4325
+ if (!address || !chain) return;
4326
+ let cancelled = false;
4327
+ setState((s) => ({ ...s, loading: true }));
4328
+ (async () => {
4329
+ try {
4330
+ if (kind === "evm") {
4331
+ const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
4332
+ if (!rpcUrl) {
4333
+ if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
4334
+ return;
4335
+ }
4336
+ const { JsonRpcProvider, formatEther } = await import("ethers");
4337
+ const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
4338
+ if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
4339
+ } else {
4340
+ const cluster = chain.slice("solana:".length);
4341
+ const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
4342
+ const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
4343
+ const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
4344
+ if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
4345
+ }
4346
+ } catch {
4347
+ if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
4348
+ }
4349
+ })();
4350
+ return () => {
4351
+ cancelled = true;
4352
+ };
4353
+ }, [kind, address, chain, rpcUrls]);
4354
+ return state;
4355
+ }
4356
+ function formatBalance(value) {
4357
+ if (value == null) return "\u2014";
4358
+ const n = Number(value);
4359
+ if (!Number.isFinite(n)) return value;
4360
+ return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
4361
+ }
4362
+ function AddressRow({
4363
+ label,
4364
+ address,
4365
+ kind,
4366
+ chain,
4367
+ rpcUrls
4368
+ }) {
4369
+ const balance = useBalance(kind, address, chain, rpcUrls);
3850
4370
  const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
3851
4371
  return /* @__PURE__ */ React3.createElement("div", { style: {
3852
4372
  background: "var(--dc-surface)",
@@ -3854,7 +4374,233 @@ function AddressRow({ label, address }) {
3854
4374
  borderRadius: 10,
3855
4375
  padding: "10px 14px",
3856
4376
  marginBottom: 8
3857
- } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 5 } }, label), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ React3.createElement(CopyButton, { value: address })));
4377
+ } }, /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase" } }, label), /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, fontFamily: "monospace", color: "var(--dc-text)" } }, balance.loading ? "\u2026" : `${formatBalance(balance.value)} ${balance.symbol}`)), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ React3.createElement(CopyButton, { value: address })));
4378
+ }
4379
+ function SignInView({
4380
+ onGoogleSignIn,
4381
+ onEmailStart,
4382
+ onEmailVerify,
4383
+ authMethods,
4384
+ resumeHint
4385
+ }) {
4386
+ const [googleLoading, setGoogleLoading] = useState2(false);
4387
+ const [email, setEmail] = useState2("");
4388
+ const [code, setCode] = useState2("");
4389
+ const [emailSent, setEmailSent] = useState2(false);
4390
+ const [emailLoading, setEmailLoading] = useState2(false);
4391
+ const [error, setError] = useState2("");
4392
+ async function handleGoogle() {
4393
+ setError("");
4394
+ setGoogleLoading(true);
4395
+ try {
4396
+ await onGoogleSignIn();
4397
+ } catch (e) {
4398
+ setError(e.message);
4399
+ setGoogleLoading(false);
4400
+ }
4401
+ }
4402
+ async function handleEmailStart(e) {
4403
+ e.preventDefault();
4404
+ setError("");
4405
+ setEmailLoading(true);
4406
+ try {
4407
+ await onEmailStart(email);
4408
+ setEmailSent(true);
4409
+ } catch (e2) {
4410
+ setError(e2.message);
4411
+ } finally {
4412
+ setEmailLoading(false);
4413
+ }
4414
+ }
4415
+ async function handleEmailVerify(e) {
4416
+ e.preventDefault();
4417
+ setError("");
4418
+ setEmailLoading(true);
4419
+ try {
4420
+ await onEmailVerify(email, code);
4421
+ } catch (e2) {
4422
+ setError(e2.message);
4423
+ } finally {
4424
+ setEmailLoading(false);
4425
+ }
4426
+ }
4427
+ const inputStyle = {
4428
+ width: "100%",
4429
+ background: "var(--dc-surface)",
4430
+ border: "1px solid var(--dc-border)",
4431
+ borderRadius: 8,
4432
+ color: "var(--dc-text)",
4433
+ padding: "9px 12px",
4434
+ fontSize: 13,
4435
+ outline: "none",
4436
+ fontFamily: "inherit",
4437
+ boxSizing: "border-box"
4438
+ };
4439
+ return /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 16px" } }, resumeHint && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 12, color: "var(--dc-text-muted)", marginBottom: 12, textAlign: "center" } }, "Welcome back \u2014 sign in again to resume ", resumeHint), authMethods.includes("google") && /* @__PURE__ */ React3.createElement(
4440
+ "button",
4441
+ {
4442
+ onClick: handleGoogle,
4443
+ disabled: googleLoading,
4444
+ style: {
4445
+ width: "100%",
4446
+ padding: "10px 0",
4447
+ marginBottom: 10,
4448
+ background: "var(--dc-accent)",
4449
+ color: "var(--dc-accent-text)",
4450
+ border: "none",
4451
+ borderRadius: 10,
4452
+ fontSize: 13,
4453
+ fontWeight: 700,
4454
+ cursor: googleLoading ? "not-allowed" : "pointer",
4455
+ opacity: googleLoading ? 0.7 : 1,
4456
+ fontFamily: "inherit"
4457
+ }
4458
+ },
4459
+ googleLoading ? "Redirecting\u2026" : "Continue with Google"
4460
+ ), authMethods.includes("email") && /* @__PURE__ */ React3.createElement(React3.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
4461
+ "input",
4462
+ {
4463
+ type: "email",
4464
+ required: true,
4465
+ placeholder: "your@email.com",
4466
+ value: email,
4467
+ onChange: (e) => setEmail(e.target.value),
4468
+ style: { ...inputStyle, flex: 1 }
4469
+ }
4470
+ ), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
4471
+ padding: "0 16px",
4472
+ background: "var(--dc-surface)",
4473
+ border: "1px solid var(--dc-border)",
4474
+ borderRadius: 8,
4475
+ color: "var(--dc-text)",
4476
+ fontSize: 13,
4477
+ cursor: "pointer",
4478
+ fontFamily: "inherit"
4479
+ } }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
4480
+ "input",
4481
+ {
4482
+ placeholder: "6-digit code",
4483
+ value: code,
4484
+ maxLength: 6,
4485
+ autoFocus: true,
4486
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
4487
+ style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
4488
+ }
4489
+ ), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
4490
+ padding: "0 16px",
4491
+ background: "var(--dc-accent)",
4492
+ color: "var(--dc-accent-text)",
4493
+ border: "none",
4494
+ borderRadius: 8,
4495
+ fontSize: 13,
4496
+ fontWeight: 700,
4497
+ cursor: "pointer",
4498
+ fontFamily: "inherit"
4499
+ } }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4500
+ }
4501
+ function RecoverySection({
4502
+ addresses,
4503
+ onRotateShares,
4504
+ onExportPortableBackup
4505
+ }) {
4506
+ const [expanded, setExpanded] = useState2(false);
4507
+ const [mode, setMode] = useState2(null);
4508
+ const [password, setPassword] = useState2("");
4509
+ const [passwordHint, setPasswordHint] = useState2("");
4510
+ const [confirmed, setConfirmed] = useState2(false);
4511
+ const [busy, setBusy] = useState2(false);
4512
+ const [result, setResult] = useState2(null);
4513
+ const [error, setError] = useState2("");
4514
+ function reset() {
4515
+ setMode(null);
4516
+ setPassword("");
4517
+ setPasswordHint("");
4518
+ setConfirmed(false);
4519
+ setError("");
4520
+ }
4521
+ async function handleGenerate() {
4522
+ if (!addresses?.evm || !mode) return;
4523
+ if (password.length < 8) {
4524
+ setError("Password must be at least 8 characters.");
4525
+ return;
4526
+ }
4527
+ if (mode === "portable" && !confirmed) {
4528
+ setError("Confirm you understand the risk first.");
4529
+ return;
4530
+ }
4531
+ setBusy(true);
4532
+ setError("");
4533
+ try {
4534
+ if (mode === "share") {
4535
+ const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
4536
+ downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
4537
+ setResult({ kind: "share", version: file.shareSetVersion });
4538
+ } else {
4539
+ const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
4540
+ downloadJson(file, `decane-PORTABLE-BACKUP-${addresses.evm.slice(0, 8)}.json`);
4541
+ setResult({ kind: "portable", version: file.shareSetVersion });
4542
+ }
4543
+ reset();
4544
+ } catch (e) {
4545
+ setError(e.message);
4546
+ } finally {
4547
+ setBusy(false);
4548
+ }
4549
+ }
4550
+ const inputStyle = {
4551
+ width: "100%",
4552
+ background: "var(--dc-bg)",
4553
+ border: "1px solid var(--dc-border)",
4554
+ borderRadius: 8,
4555
+ color: "var(--dc-text)",
4556
+ padding: "8px 10px",
4557
+ fontSize: 12,
4558
+ outline: "none",
4559
+ fontFamily: "inherit",
4560
+ boxSizing: "border-box"
4561
+ };
4562
+ const btnStyle = {
4563
+ padding: "8px 12px",
4564
+ borderRadius: 8,
4565
+ fontSize: 12,
4566
+ fontWeight: 700,
4567
+ cursor: "pointer",
4568
+ fontFamily: "inherit",
4569
+ border: "none"
4570
+ };
4571
+ return /* @__PURE__ */ React3.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ React3.createElement(
4572
+ "button",
4573
+ {
4574
+ onClick: () => setExpanded((v) => !v),
4575
+ style: {
4576
+ width: "100%",
4577
+ padding: "10px 14px",
4578
+ background: "var(--dc-surface)",
4579
+ border: "none",
4580
+ display: "flex",
4581
+ alignItems: "center",
4582
+ justifyContent: "space-between",
4583
+ cursor: "pointer",
4584
+ fontFamily: "inherit"
4585
+ }
4586
+ },
4587
+ /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
4588
+ /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
4589
+ ), expanded && /* @__PURE__ */ React3.createElement("div", { style: { padding: "12px 14px", borderTop: "1px solid var(--dc-border)" } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5, margin: "0 0 10px" } }, "Your wallet is split into three shares \u2014 device, server, and one that's yours to keep. Any two reconstruct it; one alone reveals nothing."), !mode && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: () => {
4590
+ reset();
4591
+ setMode("share");
4592
+ }, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ React3.createElement("button", { onClick: () => {
4593
+ reset();
4594
+ setMode("portable");
4595
+ }, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate portable backup (Decane-is-down)")), mode === "share" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", margin: 0 } }, "One share \u2014 useless alone, safe to keep in a password manager or cloud drive. Generating a new one invalidates any previous recovery-share file."), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), mode === "portable" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("div", { style: {
4596
+ background: "rgba(255,85,51,0.08)",
4597
+ border: "1px solid rgba(255,85,51,0.3)",
4598
+ borderRadius: 8,
4599
+ padding: "8px 10px",
4600
+ fontSize: 11,
4601
+ color: "#ff9977",
4602
+ lineHeight: 1.5
4603
+ } }, /* @__PURE__ */ React3.createElement("strong", { style: { display: "block", marginBottom: 3, color: "#ff5533" } }, "This file controls your wallet"), "Contains two shares \u2014 file plus password gives complete control. Keep it offline, not a cloud drive. This also invalidates your current recovery-share file."), /* @__PURE__ */ React3.createElement("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--dc-text)" } }, /* @__PURE__ */ React3.createElement("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), style: { width: "auto" } }), "I understand this is a bearer secret"), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy || !confirmed, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), result && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5 } }, result.kind === "share" ? "Recovery-share file" : "Portable backup", " downloaded \u2014 share set is now version ", result.version, ". Any previous recovery file no longer works.")));
3858
4604
  }
3859
4605
  function SocialWalletModal({
3860
4606
  open,
@@ -3862,7 +4608,15 @@ function SocialWalletModal({
3862
4608
  theme = "auto",
3863
4609
  socialAuth,
3864
4610
  onUnlock,
3865
- onDisconnect
4611
+ onDisconnect,
4612
+ onGoogleSignIn,
4613
+ onEmailStart,
4614
+ onEmailVerify,
4615
+ authMethods = ["google", "email"],
4616
+ onRotateShares,
4617
+ onExportPortableBackup,
4618
+ rpcUrls,
4619
+ chains
3866
4620
  }) {
3867
4621
  const [timer, setTimer] = useState2("");
3868
4622
  const [unlocking, setUnlocking] = useState2(false);
@@ -3911,6 +4665,9 @@ function SocialWalletModal({
3911
4665
  setDisconnecting(false);
3912
4666
  }
3913
4667
  }
4668
+ const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
4669
+ const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
4670
+ const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
3914
4671
  const modal = /* @__PURE__ */ React3.createElement(
3915
4672
  "div",
3916
4673
  {
@@ -3948,10 +4705,14 @@ function SocialWalletModal({
3948
4705
  borderRadius: 20,
3949
4706
  width: "100%",
3950
4707
  maxWidth: 380,
3951
- boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
3952
- overflow: "hidden"
4708
+ maxHeight: "90vh",
4709
+ overflowY: "auto",
4710
+ boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
3953
4711
  } }, /* @__PURE__ */ React3.createElement("div", { style: {
3954
4712
  height: 2,
4713
+ position: "sticky",
4714
+ top: 0,
4715
+ zIndex: 1,
3955
4716
  background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
3956
4717
  } }), /* @__PURE__ */ React3.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React3.createElement("div", { style: {
3957
4718
  width: 34,
@@ -3980,7 +4741,7 @@ function SocialWalletModal({
3980
4741
  strokeLinecap: "round",
3981
4742
  strokeLinejoin: "round"
3982
4743
  }
3983
- ))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split between device & server")), /* @__PURE__ */ React3.createElement(
4744
+ ))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split three ways")), /* @__PURE__ */ React3.createElement(
3984
4745
  "button",
3985
4746
  {
3986
4747
  onClick: onClose,
@@ -4000,7 +4761,16 @@ function SocialWalletModal({
4000
4761
  }
4001
4762
  },
4002
4763
  /* @__PURE__ */ React3.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ React3.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
4003
- )), /* @__PURE__ */ React3.createElement("div", { style: {
4764
+ )), showSignIn ? /* @__PURE__ */ React3.createElement(
4765
+ SignInView,
4766
+ {
4767
+ onGoogleSignIn,
4768
+ onEmailStart,
4769
+ onEmailVerify,
4770
+ authMethods,
4771
+ resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
4772
+ }
4773
+ ) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
4004
4774
  margin: "0 16px 14px",
4005
4775
  padding: "8px 14px",
4006
4776
  background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
@@ -4020,7 +4790,14 @@ function SocialWalletModal({
4020
4790
  fontSize: 12,
4021
4791
  fontWeight: 700,
4022
4792
  color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
4023
- } }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 16px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ React3.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm }), socialAuth.addresses?.solana && /* @__PURE__ */ React3.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana }), !socialAuth.addresses?.evm && !socialAuth.addresses?.solana && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", padding: "16px 0", color: "var(--dc-text-muted)", fontSize: 13 } }, "Unlock wallet to reveal addresses")), /* @__PURE__ */ React3.createElement("div", { style: {
4793
+ } }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 14px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ React3.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm, kind: "evm", chain: evmChain, rpcUrls }), socialAuth.addresses?.solana && /* @__PURE__ */ React3.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana, kind: "solana", chain: solChain, rpcUrls })), socialAuth.isUnlocked && /* @__PURE__ */ React3.createElement(
4794
+ RecoverySection,
4795
+ {
4796
+ addresses: socialAuth.addresses,
4797
+ onRotateShares,
4798
+ onExportPortableBackup
4799
+ }
4800
+ ), /* @__PURE__ */ React3.createElement("div", { style: {
4024
4801
  padding: "14px 16px",
4025
4802
  borderTop: "1px solid var(--dc-border)",
4026
4803
  display: "flex",
@@ -4065,7 +4842,7 @@ function SocialWalletModal({
4065
4842
  }
4066
4843
  },
4067
4844
  disconnecting ? "Signing out\u2026" : "Sign out"
4068
- )))
4845
+ ))))
4069
4846
  );
4070
4847
  return createPortal2(modal, document.body);
4071
4848
  }
@@ -4083,7 +4860,8 @@ var DEFAULT_SOCIAL = {
4083
4860
  addresses: null,
4084
4861
  isNewUser: false,
4085
4862
  isUnlocked: false,
4086
- sessionExpiresAt: null
4863
+ sessionExpiresAt: null,
4864
+ needsReconnect: false
4087
4865
  };
4088
4866
  var DecaneContext = createContext(null);
4089
4867
  function hasSocial(mode) {
@@ -4143,7 +4921,7 @@ function DecaneKit({ config, children }) {
4143
4921
  sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
4144
4922
  const saved = sdk.getAddresses();
4145
4923
  if (saved) {
4146
- setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
4924
+ setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4147
4925
  }
4148
4926
  const params = new URLSearchParams(window.location.search);
4149
4927
  if (params.has("decane_jwt") || params.has("decane_error")) {
@@ -4157,7 +4935,8 @@ function DecaneKit({ config, children }) {
4157
4935
  addresses: result.addresses,
4158
4936
  isNewUser: result.isNewUser,
4159
4937
  isUnlocked: sdk?.isUnlocked() ?? false,
4160
- sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
4938
+ sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4939
+ needsReconnect: false
4161
4940
  });
4162
4941
  }
4163
4942
  const connectWithGoogle = useCallback3(async () => {
@@ -4184,7 +4963,8 @@ function DecaneKit({ config, children }) {
4184
4963
  setSocialAuth((p) => ({
4185
4964
  ...p,
4186
4965
  isUnlocked: sdk.isUnlocked(),
4187
- sessionExpiresAt: sdk.sessionExpiresAt()
4966
+ sessionExpiresAt: sdk.sessionExpiresAt(),
4967
+ needsReconnect: false
4188
4968
  }));
4189
4969
  }, []);
4190
4970
  const socialDisconnect = useCallback3(async () => {
@@ -4208,6 +4988,31 @@ function DecaneKit({ config, children }) {
4208
4988
  if (!sdk) throw new Error("Social wallet not connected");
4209
4989
  return sdk.signSolanaTransaction(tx);
4210
4990
  }, []);
4991
+ const socialRotateShares = useCallback3(async (opts) => {
4992
+ const sdk = socialRef.current;
4993
+ if (!sdk) throw new Error("Social wallet not connected");
4994
+ return sdk.rotateShares(opts);
4995
+ }, []);
4996
+ const socialExportPortableBackup = useCallback3(async (opts) => {
4997
+ const sdk = socialRef.current;
4998
+ if (!sdk) throw new Error("Social wallet not connected");
4999
+ return sdk.exportPortableBackup(opts);
5000
+ }, []);
5001
+ const socialHasRecoveryShare = useCallback3(async () => {
5002
+ const sdk = socialRef.current;
5003
+ if (!sdk) throw new Error("Social wallet not connected");
5004
+ return sdk.hasRecoveryShare();
5005
+ }, []);
5006
+ const socialShouldPromptRecovery = useCallback3(async () => {
5007
+ const sdk = socialRef.current;
5008
+ if (!sdk) throw new Error("Social wallet not connected");
5009
+ return sdk.shouldPromptRecovery();
5010
+ }, []);
5011
+ const socialDismissRecoveryPrompt = useCallback3(async () => {
5012
+ const sdk = socialRef.current;
5013
+ if (!sdk) throw new Error("Social wallet not connected");
5014
+ return sdk.dismissRecoveryPrompt();
5015
+ }, []);
4211
5016
  const handleWalletConnect = useCallback3(
4212
5017
  (wallet, preference) => {
4213
5018
  walletRef.current.connect(wallet, preference).catch(console.error);
@@ -4235,6 +5040,11 @@ function DecaneKit({ config, children }) {
4235
5040
  socialSignMessage,
4236
5041
  socialSendTransaction,
4237
5042
  socialSignSolanaTransaction,
5043
+ socialRotateShares,
5044
+ socialExportPortableBackup,
5045
+ socialHasRecoveryShare,
5046
+ socialShouldPromptRecovery,
5047
+ socialDismissRecoveryPrompt,
4238
5048
  socialModalOpen,
4239
5049
  openSocialModal,
4240
5050
  closeSocialModal
@@ -4265,7 +5075,15 @@ function DecaneKit({ config, children }) {
4265
5075
  theme,
4266
5076
  socialAuth,
4267
5077
  onUnlock: socialUnlock,
4268
- onDisconnect: socialDisconnect
5078
+ onDisconnect: socialDisconnect,
5079
+ onGoogleSignIn: connectWithGoogle,
5080
+ onEmailStart: connectWithEmail,
5081
+ onEmailVerify: verifyEmailCode2,
5082
+ authMethods: config?.social?.authMethods ?? ["google", "email"],
5083
+ onRotateShares: socialRotateShares,
5084
+ onExportPortableBackup: socialExportPortableBackup,
5085
+ rpcUrls: config?.social?.rpcUrls,
5086
+ chains: config?.social?.chains
4269
5087
  }
4270
5088
  )
4271
5089
  );
@@ -4353,6 +5171,11 @@ function useSocialAuth() {
4353
5171
  isNewUser: socialAuth.isNewUser,
4354
5172
  addresses: socialAuth.addresses,
4355
5173
  sessionExpiresAt: socialAuth.sessionExpiresAt,
5174
+ // True when addresses are known (from a previous visit) but there's no
5175
+ // live session — unlock() will always throw in this state (every
5176
+ // unlock requires a fresh JWT, and JWTs are never persisted). Route
5177
+ // this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
5178
+ needsReconnect: socialAuth.needsReconnect,
4356
5179
  googleLoading,
4357
5180
  emailLoading,
4358
5181
  error,
@@ -4372,6 +5195,11 @@ function useSocialWallet() {
4372
5195
  socialSignMessage,
4373
5196
  socialSendTransaction,
4374
5197
  socialSignSolanaTransaction,
5198
+ socialRotateShares,
5199
+ socialExportPortableBackup,
5200
+ socialHasRecoveryShare,
5201
+ socialShouldPromptRecovery,
5202
+ socialDismissRecoveryPrompt,
4375
5203
  openSocialModal,
4376
5204
  closeSocialModal,
4377
5205
  socialModalOpen
@@ -4421,6 +5249,26 @@ function useSocialWallet() {
4421
5249
  throw e;
4422
5250
  }
4423
5251
  }, [socialDisconnect]);
5252
+ const rotateShares = useCallback4(
5253
+ (opts) => run(() => socialRotateShares(opts)),
5254
+ [socialRotateShares]
5255
+ );
5256
+ const exportPortableBackup = useCallback4(
5257
+ (opts) => run(() => socialExportPortableBackup(opts)),
5258
+ [socialExportPortableBackup]
5259
+ );
5260
+ const hasRecoveryShare = useCallback4(
5261
+ () => run(() => socialHasRecoveryShare()),
5262
+ [socialHasRecoveryShare]
5263
+ );
5264
+ const shouldPromptRecovery = useCallback4(
5265
+ () => run(() => socialShouldPromptRecovery()),
5266
+ [socialShouldPromptRecovery]
5267
+ );
5268
+ const dismissRecoveryPrompt = useCallback4(
5269
+ () => run(() => socialDismissRecoveryPrompt()),
5270
+ [socialDismissRecoveryPrompt]
5271
+ );
4424
5272
  return {
4425
5273
  // State
4426
5274
  isConnected: !!socialAuth.addresses,
@@ -4428,6 +5276,11 @@ function useSocialWallet() {
4428
5276
  isNewUser: socialAuth.isNewUser,
4429
5277
  addresses: socialAuth.addresses,
4430
5278
  sessionExpiresAt: socialAuth.sessionExpiresAt,
5279
+ // True when addresses are known (from a previous visit) but there's no
5280
+ // live session — unlock() will always throw in this state (every
5281
+ // unlock requires a fresh JWT, and JWTs are never persisted). Route
5282
+ // this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
5283
+ needsReconnect: socialAuth.needsReconnect,
4431
5284
  // Modal
4432
5285
  modalOpen: socialModalOpen,
4433
5286
  openModal: openSocialModal,
@@ -4439,6 +5292,12 @@ function useSocialWallet() {
4439
5292
  signMessage,
4440
5293
  sendTransaction,
4441
5294
  signSolanaTransaction: signSolanaTransaction2,
5295
+ // Recovery (2-of-3 Shamir share set)
5296
+ rotateShares,
5297
+ exportPortableBackup,
5298
+ hasRecoveryShare,
5299
+ shouldPromptRecovery,
5300
+ dismissRecoveryPrompt,
4442
5301
  loading,
4443
5302
  error,
4444
5303
  clearError: () => setError(null)
@@ -4676,14 +5535,17 @@ export {
4676
5535
  DecaneError,
4677
5536
  DecaneErrorCode,
4678
5537
  DecaneKit,
5538
+ ExportNotAuthorizedError,
4679
5539
  InvalidApiKeyError,
4680
- InvalidPhraseError,
4681
5540
  InvalidPinError,
5541
+ InvalidRecoveryFileError,
4682
5542
  NetworkError,
4683
5543
  NewDeviceError,
4684
5544
  OriginNotAllowedError,
4685
5545
  PromptPinNotConfiguredError,
5546
+ ReconnectRequiredError,
4686
5547
  RecoveryFailedError,
5548
+ RecoveryWalletMismatchError,
4687
5549
  SessionExpiredError,
4688
5550
  ShareCorruptedError,
4689
5551
  SocialWalletModal,
@@ -4691,6 +5553,7 @@ export {
4691
5553
  UserCancelledError,
4692
5554
  WalletSelector,
4693
5555
  WebAuthnNotSupportedError,
5556
+ WrongPasswordError,
4694
5557
  createDecane,
4695
5558
  createDecaneConnect,
4696
5559
  startEmailAuth,