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.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,148 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/social/errors.ts
34
+ var errors_exports = {};
35
+ __export(errors_exports, {
36
+ AttestationError: () => AttestationError,
37
+ CallSequenceError: () => CallSequenceError,
38
+ DecaneConnectError: () => DecaneConnectError,
39
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
40
+ InvalidApiKeyError: () => InvalidApiKeyError,
41
+ InvalidPinError: () => InvalidPinError,
42
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
43
+ NetworkError: () => NetworkError,
44
+ NewDeviceError: () => NewDeviceError,
45
+ OriginNotAllowedError: () => OriginNotAllowedError,
46
+ PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
47
+ ReconnectRequiredError: () => ReconnectRequiredError,
48
+ RecoveryFailedError: () => RecoveryFailedError,
49
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
50
+ SessionExpiredError: () => SessionExpiredError,
51
+ ShareCorruptedError: () => ShareCorruptedError,
52
+ UnlockRequiredError: () => UnlockRequiredError,
53
+ UserCancelledError: () => UserCancelledError,
54
+ WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
55
+ WrongPasswordError: () => WrongPasswordError
56
+ });
57
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
58
+ var init_errors = __esm({
59
+ "src/social/errors.ts"() {
60
+ "use strict";
61
+ DecaneConnectError = class extends Error {
62
+ constructor(code, message, retryable = false) {
63
+ super(message);
64
+ this.name = "DecaneConnectError";
65
+ this.code = code;
66
+ this.retryable = retryable;
67
+ }
68
+ };
69
+ InvalidApiKeyError = class extends DecaneConnectError {
70
+ constructor() {
71
+ super("INVALID_API_KEY", "API key is invalid or revoked");
72
+ }
73
+ };
74
+ OriginNotAllowedError = class extends DecaneConnectError {
75
+ constructor(origin) {
76
+ super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
77
+ }
78
+ };
79
+ SessionExpiredError = class extends DecaneConnectError {
80
+ constructor() {
81
+ super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
82
+ }
83
+ };
84
+ UnlockRequiredError = class extends DecaneConnectError {
85
+ constructor() {
86
+ super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
87
+ }
88
+ };
89
+ ReconnectRequiredError = class extends DecaneConnectError {
90
+ constructor() {
91
+ super(
92
+ "RECONNECT_REQUIRED",
93
+ "Your session ended \u2014 sign in again to unlock this wallet",
94
+ true
95
+ );
96
+ }
97
+ };
98
+ UserCancelledError = class extends DecaneConnectError {
99
+ constructor() {
100
+ super("USER_CANCELLED", "User cancelled the operation", true);
101
+ }
102
+ };
103
+ WebAuthnNotSupportedError = class extends DecaneConnectError {
104
+ constructor() {
105
+ super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
106
+ }
107
+ };
108
+ InvalidPinError = class extends DecaneConnectError {
109
+ constructor() {
110
+ super("INVALID_PIN", "Incorrect PIN", true);
111
+ }
112
+ };
113
+ CallSequenceError = class extends DecaneConnectError {
114
+ constructor(msg) {
115
+ super("CALL_SEQUENCE", msg);
116
+ }
117
+ };
118
+ NetworkError = class extends DecaneConnectError {
119
+ constructor() {
120
+ super("NETWORK", "Network request failed", true);
121
+ }
122
+ };
123
+ ShareCorruptedError = class extends DecaneConnectError {
124
+ constructor() {
125
+ super("SHARE_CORRUPTED", "Stored device share is corrupted");
126
+ }
127
+ };
128
+ NewDeviceError = class extends DecaneConnectError {
129
+ constructor() {
130
+ super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
131
+ }
132
+ };
133
+ PromptPinNotConfiguredError = class extends DecaneConnectError {
134
+ constructor() {
135
+ super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
136
+ }
137
+ };
138
+ AttestationError = class extends DecaneConnectError {
139
+ constructor(msg) {
140
+ super("ATTESTATION_FAILED", msg);
141
+ }
142
+ };
143
+ RecoveryFailedError = class extends DecaneConnectError {
144
+ constructor(reason) {
145
+ super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
146
+ }
147
+ };
148
+ InvalidRecoveryFileError = class extends DecaneConnectError {
149
+ constructor() {
150
+ super("INVALID_RECOVERY_FILE", "This does not look like a valid recovery file", false);
151
+ }
152
+ };
153
+ WrongPasswordError = class extends DecaneConnectError {
154
+ constructor() {
155
+ super("WRONG_PASSWORD", "Incorrect password for this recovery file", true);
156
+ }
157
+ };
158
+ RecoveryWalletMismatchError = class extends DecaneConnectError {
159
+ constructor() {
160
+ super(
161
+ "RECOVERY_WALLET_MISMATCH",
162
+ "This recovery file is for a different wallet than the account you are signed into",
163
+ false
164
+ );
165
+ }
166
+ };
167
+ ExportNotAuthorizedError = class extends DecaneConnectError {
168
+ constructor() {
169
+ super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
170
+ }
171
+ };
172
+ }
173
+ });
174
+
30
175
  // src/index.ts
31
176
  var index_exports = {};
32
177
  __export(index_exports, {
@@ -38,14 +183,17 @@ __export(index_exports, {
38
183
  DecaneError: () => DecaneError,
39
184
  DecaneErrorCode: () => DecaneErrorCode,
40
185
  DecaneKit: () => DecaneKit,
186
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
41
187
  InvalidApiKeyError: () => InvalidApiKeyError,
42
- InvalidPhraseError: () => InvalidPhraseError,
43
188
  InvalidPinError: () => InvalidPinError,
189
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
44
190
  NetworkError: () => NetworkError,
45
191
  NewDeviceError: () => NewDeviceError,
46
192
  OriginNotAllowedError: () => OriginNotAllowedError,
47
193
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
194
+ ReconnectRequiredError: () => ReconnectRequiredError,
48
195
  RecoveryFailedError: () => RecoveryFailedError,
196
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
49
197
  SessionExpiredError: () => SessionExpiredError,
50
198
  ShareCorruptedError: () => ShareCorruptedError,
51
199
  SocialWalletModal: () => SocialWalletModal,
@@ -53,6 +201,7 @@ __export(index_exports, {
53
201
  UserCancelledError: () => UserCancelledError,
54
202
  WalletSelector: () => WalletSelector,
55
203
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
204
+ WrongPasswordError: () => WrongPasswordError,
56
205
  createDecane: () => createDecane,
57
206
  createDecaneConnect: () => createDecaneConnect,
58
207
  startEmailAuth: () => startEmailAuth,
@@ -1806,91 +1955,7 @@ function createDecane(config = {}) {
1806
1955
  // src/social/client.ts
1807
1956
  var import_ethers2 = require("ethers");
1808
1957
  var import_web32 = require("@solana/web3.js");
1809
-
1810
- // src/social/errors.ts
1811
- var DecaneConnectError = class extends Error {
1812
- constructor(code, message, retryable = false) {
1813
- super(message);
1814
- this.name = "DecaneConnectError";
1815
- this.code = code;
1816
- this.retryable = retryable;
1817
- }
1818
- };
1819
- var InvalidApiKeyError = class extends DecaneConnectError {
1820
- constructor() {
1821
- super("INVALID_API_KEY", "API key is invalid or revoked");
1822
- }
1823
- };
1824
- var OriginNotAllowedError = class extends DecaneConnectError {
1825
- constructor(origin) {
1826
- super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
1827
- }
1828
- };
1829
- var SessionExpiredError = class extends DecaneConnectError {
1830
- constructor() {
1831
- super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
1832
- }
1833
- };
1834
- var UnlockRequiredError = class extends DecaneConnectError {
1835
- constructor() {
1836
- super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
1837
- }
1838
- };
1839
- var UserCancelledError = class extends DecaneConnectError {
1840
- constructor() {
1841
- super("USER_CANCELLED", "User cancelled the operation", true);
1842
- }
1843
- };
1844
- var WebAuthnNotSupportedError = class extends DecaneConnectError {
1845
- constructor() {
1846
- super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
1847
- }
1848
- };
1849
- var InvalidPinError = class extends DecaneConnectError {
1850
- constructor() {
1851
- super("INVALID_PIN", "Incorrect PIN", true);
1852
- }
1853
- };
1854
- var CallSequenceError = class extends DecaneConnectError {
1855
- constructor(msg) {
1856
- super("CALL_SEQUENCE", msg);
1857
- }
1858
- };
1859
- var NetworkError = class extends DecaneConnectError {
1860
- constructor() {
1861
- super("NETWORK", "Network request failed", true);
1862
- }
1863
- };
1864
- var ShareCorruptedError = class extends DecaneConnectError {
1865
- constructor() {
1866
- super("SHARE_CORRUPTED", "Stored device share is corrupted");
1867
- }
1868
- };
1869
- var NewDeviceError = class extends DecaneConnectError {
1870
- constructor() {
1871
- super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
1872
- }
1873
- };
1874
- var PromptPinNotConfiguredError = class extends DecaneConnectError {
1875
- constructor() {
1876
- super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
1877
- }
1878
- };
1879
- var AttestationError = class extends DecaneConnectError {
1880
- constructor(msg) {
1881
- super("ATTESTATION_FAILED", msg);
1882
- }
1883
- };
1884
- var RecoveryFailedError = class extends DecaneConnectError {
1885
- constructor(reason) {
1886
- super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
1887
- }
1888
- };
1889
- var InvalidPhraseError = class extends DecaneConnectError {
1890
- constructor() {
1891
- super("INVALID_PHRASE", "Recovery phrase is invalid or corrupted", true);
1892
- }
1893
- };
1958
+ init_errors();
1894
1959
 
1895
1960
  // src/social/events.ts
1896
1961
  var EventEmitter = class {
@@ -1910,6 +1975,7 @@ var EventEmitter = class {
1910
1975
  };
1911
1976
 
1912
1977
  // src/social/api.ts
1978
+ init_errors();
1913
1979
  var ERROR_MAP = {
1914
1980
  INVALID_API_KEY: InvalidApiKeyError,
1915
1981
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
@@ -1980,13 +2046,26 @@ var ApiClient = class {
1980
2046
  })
1981
2047
  });
1982
2048
  }
2049
+ // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
2050
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
2051
+ return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
2052
+ headers: { Authorization: `Bearer ${jwt}` }
2053
+ });
2054
+ }
2055
+ async postWebAuthnRegister(jwt, response, origin) {
2056
+ await this._request("/webauthn/register", {
2057
+ method: "POST",
2058
+ headers: { Authorization: `Bearer ${jwt}` },
2059
+ body: JSON.stringify({ response, origin })
2060
+ });
2061
+ }
1983
2062
  async revoke(jwt) {
1984
2063
  await this._request("/auth/revoke", {
1985
2064
  method: "POST",
1986
2065
  headers: { Authorization: `Bearer ${jwt}` }
1987
2066
  });
1988
2067
  }
1989
- // ── Recovery (passkey-sync backup + phrase-saved flag) ────────────────────
2068
+ // ── Recovery (passkey-sync backup + recovery-share-saved flag) ────────────
1990
2069
  async hasRecoveryBackup(jwt) {
1991
2070
  const data = await this._request("/recovery/backup/exists", {
1992
2071
  headers: { Authorization: `Bearer ${jwt}` }
@@ -2027,8 +2106,19 @@ var ApiClient = class {
2027
2106
  headers: { Authorization: `Bearer ${jwt}` }
2028
2107
  });
2029
2108
  }
2030
- async markPhraseSaved(jwt) {
2031
- await this._request("/recovery/phrase-saved", {
2109
+ async markRecoveryShareSaved(jwt) {
2110
+ await this._request("/recovery/share-saved", {
2111
+ method: "POST",
2112
+ headers: { Authorization: `Bearer ${jwt}` }
2113
+ });
2114
+ }
2115
+ async getRecoveryStatus(jwt) {
2116
+ return this._request("/recovery/status", {
2117
+ headers: { Authorization: `Bearer ${jwt}` }
2118
+ });
2119
+ }
2120
+ async dismissRecoveryNag(jwt) {
2121
+ await this._request("/recovery/nag-dismissed", {
2032
2122
  method: "POST",
2033
2123
  headers: { Authorization: `Bearer ${jwt}` }
2034
2124
  });
@@ -2037,8 +2127,8 @@ var ApiClient = class {
2037
2127
  function bytesToB64(bytes) {
2038
2128
  return btoa(String.fromCharCode(...bytes));
2039
2129
  }
2040
- function b64ToBytes(b642) {
2041
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
2130
+ function b64ToBytes(b644) {
2131
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2042
2132
  }
2043
2133
 
2044
2134
  // src/social/storage.ts
@@ -2064,8 +2154,12 @@ var Storage = class {
2064
2154
  }
2065
2155
  };
2066
2156
 
2157
+ // src/social/teeClient.ts
2158
+ var import_browser = require("@simplewebauthn/browser");
2159
+
2067
2160
  // src/social/attestation.ts
2068
2161
  var import_dcap_qvl = require("@phala/dcap-qvl");
2162
+ init_errors();
2069
2163
 
2070
2164
  // ../../infra/tee-measurement.json
2071
2165
  var tee_measurement_default = {
@@ -2123,11 +2217,15 @@ async function verifyTeeAttestation(teeUrl, nonce) {
2123
2217
  }
2124
2218
 
2125
2219
  // src/social/teeClient.ts
2220
+ init_errors();
2126
2221
  function bytesToBase64(b) {
2127
2222
  let s = "";
2128
2223
  for (const byte of b) s += String.fromCharCode(byte);
2129
2224
  return btoa(s);
2130
2225
  }
2226
+ function base64ToBytes(s) {
2227
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2228
+ }
2131
2229
  var TeeClient = class {
2132
2230
  constructor(teeUrl) {
2133
2231
  this.teeUrl = teeUrl;
@@ -2141,6 +2239,13 @@ var TeeClient = class {
2141
2239
  await verifyTeeAttestation(this.teeUrl, nonceHex);
2142
2240
  this.attested = true;
2143
2241
  }
2242
+ /**
2243
+ * Opens a signing session and returns the addresses the TEE derived from the
2244
+ * key it actually reconstructed — authoritative, unlike the backend's
2245
+ * recorded copy, which nothing else can verify. Returns null if the TEE
2246
+ * couldn't derive them (older signer build, or a transient share-fetch
2247
+ * failure); callers fall back to the backend's addresses in that case.
2248
+ */
2144
2249
  async openSession(deviceShare, jwt) {
2145
2250
  await this.ensureAttested();
2146
2251
  const res = await fetch(`${this.teeUrl}/signer/session`, {
@@ -2149,18 +2254,94 @@ var TeeClient = class {
2149
2254
  body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2150
2255
  });
2151
2256
  if (!res.ok) throw new Error("session open failed");
2152
- this.handle = (await res.json()).handle;
2257
+ const body = await res.json();
2258
+ this.handle = body.handle;
2259
+ return body.addresses ?? null;
2153
2260
  }
2154
- /** Verify a candidate device share (from a recovery phrase) without exposing shares. */
2155
- async verifyRecoveryCandidate(deviceShare, jwt) {
2261
+ /**
2262
+ * Flow B: exchange a recovery share (from a recovery-share file) for a
2263
+ * fresh device share. The server verifies the reconstructed master
2264
+ * actually matches this account's addresses before returning anything —
2265
+ * see the security note in the TEE's /signer/recover handler. Rotates the
2266
+ * whole share set: the caller MUST persist the returned recoveryShare as a
2267
+ * new recovery-share file, since the old one stops working immediately.
2268
+ */
2269
+ async recoverWithShare(jwt, recoveryShare) {
2156
2270
  await this.ensureAttested();
2157
- const res = await fetch(`${this.teeUrl}/signer/verify-recovery`, {
2271
+ const res = await fetch(`${this.teeUrl}/signer/recover`, {
2158
2272
  method: "POST",
2159
2273
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2160
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2274
+ body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
2161
2275
  });
2162
- if (!res.ok) return false;
2163
- return (await res.json()).verified === true;
2276
+ if (res.status === 403) {
2277
+ const { RecoveryWalletMismatchError: RecoveryWalletMismatchError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2278
+ throw new RecoveryWalletMismatchError2();
2279
+ }
2280
+ if (!res.ok) throw new Error("recovery failed");
2281
+ const body = await res.json();
2282
+ return {
2283
+ deviceShare: base64ToBytes(body.deviceShare),
2284
+ recoveryShare: base64ToBytes(body.recoveryShare),
2285
+ shareSetVersion: body.shareSetVersion
2286
+ };
2287
+ }
2288
+ /**
2289
+ * Request a fresh WebAuthn assertion challenge and complete it in the
2290
+ * browser via @simplewebauthn/browser's startAuthentication(), which
2291
+ * handles all the base64url (de)serialization the raw navigator.credentials
2292
+ * API requires. Shared by rotateShares() and exportPortableBackup() — both
2293
+ * require proof of fresh authenticator possession, not just an active session.
2294
+ */
2295
+ async completeAssertion(jwt) {
2296
+ const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2297
+ method: "POST",
2298
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2299
+ body: JSON.stringify({ rpId: window.location.hostname })
2300
+ });
2301
+ if (!res.ok) throw new ExportNotAuthorizedError();
2302
+ const { challengeId, options } = await res.json();
2303
+ let assertion;
2304
+ try {
2305
+ assertion = await (0, import_browser.startAuthentication)({ optionsJSON: options });
2306
+ } catch {
2307
+ const { UserCancelledError: UserCancelledError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2308
+ throw new UserCancelledError2();
2309
+ }
2310
+ return { challengeId, assertion };
2311
+ }
2312
+ /** Voluntary rotation — e.g. after a suspected device compromise, or to invalidate an old recovery file. */
2313
+ async rotateShares(jwt) {
2314
+ if (!this.handle) throw new Error("no session");
2315
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2316
+ const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2317
+ method: "POST",
2318
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2319
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2320
+ });
2321
+ if (!res.ok) throw new ExportNotAuthorizedError();
2322
+ const body = await res.json();
2323
+ return {
2324
+ deviceShare: base64ToBytes(body.deviceShare),
2325
+ recoveryShare: base64ToBytes(body.recoveryShare),
2326
+ shareSetVersion: body.shareSetVersion
2327
+ };
2328
+ }
2329
+ /** The two-share bearer portable backup — see recovery/portableBackup.ts. Rotates the set on export. */
2330
+ async exportPortableBackup(jwt) {
2331
+ if (!this.handle) throw new Error("no session");
2332
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2333
+ const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2334
+ method: "POST",
2335
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2336
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2337
+ });
2338
+ if (!res.ok) throw new ExportNotAuthorizedError();
2339
+ const body = await res.json();
2340
+ return {
2341
+ deviceShare: base64ToBytes(body.deviceShare),
2342
+ recoveryShare: base64ToBytes(body.recoveryShare),
2343
+ shareSetVersion: body.shareSetVersion
2344
+ };
2164
2345
  }
2165
2346
  hasSession() {
2166
2347
  return this.handle !== null;
@@ -2193,6 +2374,7 @@ var TeeClient = class {
2193
2374
  };
2194
2375
 
2195
2376
  // src/social/session.ts
2377
+ init_errors();
2196
2378
  var SessionManager = class {
2197
2379
  constructor(events, durationMs, teeUrl) {
2198
2380
  this.events = events;
@@ -2207,9 +2389,24 @@ var SessionManager = class {
2207
2389
  window.addEventListener("pagehide", () => this.end());
2208
2390
  }
2209
2391
  }
2210
- async start(deviceShare, jwt, addresses) {
2392
+ /**
2393
+ * Opens the TEE session and resolves which addresses this wallet really has.
2394
+ *
2395
+ * `fallback` is whatever the backend recorded; the TEE's answer is derived
2396
+ * from the signing key itself and therefore outranks it. Returns the
2397
+ * resolved addresses so callers can propagate the corrected values rather
2398
+ * than continuing to use a stale record.
2399
+ */
2400
+ async start(deviceShare, jwt, fallback) {
2211
2401
  this.tee = new TeeClient(this.teeUrl);
2212
- await this.tee.openSession(deviceShare, jwt);
2402
+ const verified = await this.tee.openSession(deviceShare, jwt);
2403
+ const addresses = verified ?? fallback;
2404
+ if (!addresses) {
2405
+ this.tee = null;
2406
+ throw new Error(
2407
+ "No addresses available for this wallet \u2014 the signer could not derive them and none are recorded"
2408
+ );
2409
+ }
2213
2410
  this.evmAddress = addresses.evm;
2214
2411
  this.solanaAddress = addresses.solana;
2215
2412
  this.expiresAt = Date.now() + this.durationMs;
@@ -2219,6 +2416,7 @@ var SessionManager = class {
2219
2416
  this.events.emit("session-expired");
2220
2417
  }, this.durationMs);
2221
2418
  this.events.emit("session-started");
2419
+ return addresses;
2222
2420
  }
2223
2421
  end() {
2224
2422
  void this.tee?.closeSession();
@@ -2248,24 +2446,32 @@ var SessionManager = class {
2248
2446
  };
2249
2447
 
2250
2448
  // src/social/crypto/shares.ts
2251
- function xorBytes(a, b) {
2252
- if (a.length !== b.length) throw new Error("xorBytes: length mismatch");
2253
- const out = new Uint8Array(a.length);
2254
- for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
2255
- return out;
2256
- }
2449
+ var import_secp256k1 = require("@noble/curves/secp256k1");
2450
+ var import_shamir_secret_sharing = require("shamir-secret-sharing");
2257
2451
  function generateRandomBytes(n) {
2258
2452
  return crypto.getRandomValues(new Uint8Array(n));
2259
2453
  }
2260
- function generateWalletShares() {
2261
- const privateKey = generateRandomBytes(32);
2262
- const deviceShare = generateRandomBytes(32);
2263
- const serverShare = xorBytes(privateKey, deviceShare);
2264
- return { privateKey, deviceShare, serverShare };
2265
- }
2266
2454
  function zeroize(bytes) {
2267
2455
  bytes.fill(0);
2268
2456
  }
2457
+ function generateMaster() {
2458
+ let master;
2459
+ do {
2460
+ master = generateRandomBytes(32);
2461
+ } while (!import_secp256k1.secp256k1.utils.isValidPrivateKey(master));
2462
+ return master;
2463
+ }
2464
+ async function splitMaster(master, version = 1) {
2465
+ if (master.length !== 32) throw new Error("master must be 32 bytes");
2466
+ const parts = await (0, import_shamir_secret_sharing.split)(master, 3, 2);
2467
+ if (parts.length !== 3) throw new Error("unexpected share count");
2468
+ return { device: parts[0], server: parts[1], recovery: parts[2], version };
2469
+ }
2470
+ async function combineShares(a, b) {
2471
+ const master = await (0, import_shamir_secret_sharing.combine)([a, b]);
2472
+ if (master.length !== 32) throw new Error("reconstruction produced wrong length");
2473
+ return master;
2474
+ }
2269
2475
 
2270
2476
  // src/social/crypto/wrap.ts
2271
2477
  function buf(u) {
@@ -2310,6 +2516,8 @@ function validatePin(pin) {
2310
2516
  }
2311
2517
 
2312
2518
  // src/social/crypto/passkey.ts
2519
+ var import_browser2 = require("@simplewebauthn/browser");
2520
+ init_errors();
2313
2521
  function buf3(u) {
2314
2522
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2315
2523
  }
@@ -2317,30 +2525,24 @@ function isPasskeySupported() {
2317
2525
  return typeof window !== "undefined" && !!window.PublicKeyCredential && "isUserVerifyingPlatformAuthenticatorAvailable" in window.PublicKeyCredential;
2318
2526
  }
2319
2527
  async function registerPasskey(opts) {
2320
- const challenge = crypto.getRandomValues(new Uint8Array(32));
2321
- let cred;
2528
+ let response;
2322
2529
  try {
2323
- cred = await navigator.credentials.create({
2324
- publicKey: {
2325
- challenge: buf3(challenge),
2326
- rp: { id: opts.rpId, name: opts.rpName },
2327
- user: { id: buf3(opts.userId), name: opts.userName, displayName: opts.userName },
2328
- pubKeyCredParams: [
2329
- { type: "public-key", alg: -7 },
2330
- { type: "public-key", alg: -257 }
2331
- ],
2332
- authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
2333
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
2334
- }
2335
- });
2530
+ response = await (0, import_browser2.startRegistration)({ optionsJSON: opts.optionsJSON });
2336
2531
  } catch (e) {
2337
2532
  if (e.name === "NotAllowedError") throw new UserCancelledError();
2338
2533
  throw e;
2339
2534
  }
2340
- const ext = cred.getClientExtensionResults();
2341
- const prfResults = ext?.prf?.results;
2342
- const prfOutput = prfResults?.first ? new Uint8Array(prfResults.first) : void 0;
2343
- return { credentialId: new Uint8Array(cred.rawId), prfOutput };
2535
+ const extResults = response.clientExtensionResults;
2536
+ const prfResults = extResults?.prf?.results;
2537
+ let prfOutput;
2538
+ if (prfResults?.first) {
2539
+ prfOutput = typeof prfResults.first === "string" ? Uint8Array.from(atob(prfResults.first.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)) : new Uint8Array(prfResults.first);
2540
+ }
2541
+ const credentialId = Uint8Array.from(
2542
+ atob(response.id.replace(/-/g, "+").replace(/_/g, "/")),
2543
+ (c) => c.charCodeAt(0)
2544
+ );
2545
+ return { credentialId, prfOutput, response };
2344
2546
  }
2345
2547
  async function getPrfOutput(opts) {
2346
2548
  const challenge = crypto.getRandomValues(new Uint8Array(32));
@@ -2365,6 +2567,28 @@ async function getPrfOutput(opts) {
2365
2567
  return new Uint8Array(prfResults.first);
2366
2568
  }
2367
2569
 
2570
+ // src/social/webauthnRegistration.ts
2571
+ function bufFromBytes(u) {
2572
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2573
+ }
2574
+ async function registerWebAuthnCredential(opts) {
2575
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2576
+ const optionsJSON = {
2577
+ ...baseOptions,
2578
+ extensions: {
2579
+ ...baseOptions.extensions,
2580
+ prf: { eval: { first: bufFromBytes(opts.prfSalt) } }
2581
+ }
2582
+ };
2583
+ const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2584
+ try {
2585
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2586
+ } catch (e) {
2587
+ 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);
2588
+ }
2589
+ return { credentialId, prfOutput };
2590
+ }
2591
+
2368
2592
  // src/social/auth/email.ts
2369
2593
  async function startEmailAuth(api, email) {
2370
2594
  await api.authEmailStart(email);
@@ -2411,28 +2635,102 @@ async function fetchWrappedShareBackup(api, jwt) {
2411
2635
  return api.getRecoveryBackup(jwt);
2412
2636
  }
2413
2637
 
2414
- // src/social/recovery/phrase.ts
2415
- var import_bip39 = require("@scure/bip39");
2416
- var import_english = require("@scure/bip39/wordlists/english.js");
2417
- function shareToPhrase(deviceShare) {
2418
- if (deviceShare.length !== 32) throw new Error("deviceShare must be 32 bytes");
2419
- return (0, import_bip39.entropyToMnemonic)(deviceShare, import_english.wordlist);
2638
+ // src/social/recovery/recoveryShareFile.ts
2639
+ init_errors();
2640
+ var PBKDF2_ITERATIONS2 = 6e5;
2641
+ var FILE_VERSION = 2;
2642
+ function b642(u) {
2643
+ let s = "";
2644
+ for (const b of u) s += String.fromCharCode(b);
2645
+ return btoa(s);
2420
2646
  }
2421
- function phraseToShare(phrase) {
2422
- const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
2423
- if (!(0, import_bip39.validateMnemonic)(normalized, import_english.wordlist)) throw new InvalidPhraseError();
2424
- const entropy = (0, import_bip39.mnemonicToEntropy)(normalized, import_english.wordlist);
2425
- if (entropy.length !== 32) throw new InvalidPhraseError();
2426
- return entropy;
2647
+ function unb642(s) {
2648
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2427
2649
  }
2428
- function phraseConfirmationChallenge(phrase) {
2429
- const words = phrase.trim().split(/\s+/);
2430
- const chosen = /* @__PURE__ */ new Set();
2431
- while (chosen.size < 3) chosen.add(Math.floor(Math.random() * words.length));
2432
- return [...chosen].sort((a, b) => a - b).map((i) => ({ position: i + 1, expected: words[i] }));
2650
+ function buf4(u) {
2651
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2652
+ }
2653
+ async function deriveFileKey(password, salt, iterations) {
2654
+ const base = await crypto.subtle.importKey(
2655
+ "raw",
2656
+ new TextEncoder().encode(password),
2657
+ "PBKDF2",
2658
+ false,
2659
+ ["deriveKey"]
2660
+ );
2661
+ return crypto.subtle.deriveKey(
2662
+ { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
2663
+ base,
2664
+ { name: "AES-GCM", length: 256 },
2665
+ false,
2666
+ ["encrypt", "decrypt"]
2667
+ );
2668
+ }
2669
+ async function buildRecoveryShareFile(share, password, addresses, shareSetVersion, passwordHint) {
2670
+ if (share.length !== 33) throw new Error("share must be 33 bytes");
2671
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2672
+ if (passwordHint && passwordHint.trim() === password) {
2673
+ throw new Error("password hint must not be the password");
2674
+ }
2675
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2676
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2677
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
2678
+ const ct = new Uint8Array(
2679
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
2680
+ );
2681
+ return {
2682
+ version: FILE_VERSION,
2683
+ type: "decane-recovery-share",
2684
+ shareSetVersion,
2685
+ kdf: "pbkdf2-sha256",
2686
+ kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
2687
+ cipher: "aes-256-gcm",
2688
+ iv: b642(iv),
2689
+ ciphertext: b642(ct),
2690
+ addresses,
2691
+ passwordHint: passwordHint?.trim() || null,
2692
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2693
+ };
2694
+ }
2695
+ function parseRecoveryShareFile(json) {
2696
+ const raw = json;
2697
+ if (raw?.type === "decane-recovery") {
2698
+ throw new InvalidRecoveryFileError();
2699
+ }
2700
+ const f = json;
2701
+ 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") {
2702
+ throw new InvalidRecoveryFileError();
2703
+ }
2704
+ if (f.kdfParams.iterations < 1e5 || f.kdfParams.iterations > 5e6) {
2705
+ throw new InvalidRecoveryFileError();
2706
+ }
2707
+ return f;
2708
+ }
2709
+ async function decryptRecoveryShareFile(file, password) {
2710
+ const salt = unb642(file.kdfParams.salt);
2711
+ const iv = unb642(file.iv);
2712
+ const ct = unb642(file.ciphertext);
2713
+ const key = await deriveFileKey(password, salt, file.kdfParams.iterations);
2714
+ let share;
2715
+ try {
2716
+ share = new Uint8Array(
2717
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
2718
+ );
2719
+ } catch {
2720
+ throw new WrongPasswordError();
2721
+ }
2722
+ if (share.length !== 33) {
2723
+ share.fill(0);
2724
+ throw new InvalidRecoveryFileError();
2725
+ }
2726
+ return share;
2727
+ }
2728
+ function recoveryShareFileName(evmAddress) {
2729
+ return `decane-recovery-${evmAddress.slice(0, 8)}.json`;
2433
2730
  }
2434
2731
 
2435
2732
  // src/social/recovery/recoveryFlow.ts
2733
+ init_errors();
2436
2734
  async function recoverDeviceShare(input) {
2437
2735
  const backup = await fetchWrappedShareBackup(input.api, input.jwt);
2438
2736
  if (backup) {
@@ -2466,37 +2764,87 @@ async function recoverDeviceShare(input) {
2466
2764
  });
2467
2765
  }
2468
2766
  }
2469
- if (!input.promptForPhrase) throw new NewDeviceError();
2470
- input.onProgress?.({ type: "phrase-prompt" });
2471
- const phrase = await input.promptForPhrase();
2472
- if (!phrase) throw new NewDeviceError();
2473
- let candidate;
2474
- try {
2475
- candidate = phraseToShare(phrase);
2476
- } catch {
2477
- throw new InvalidPhraseError();
2478
- }
2479
- input.onProgress?.({ type: "phrase-verifying" });
2767
+ if (!input.promptForRecoveryFile) throw new NewDeviceError();
2768
+ input.onProgress?.({ type: "recovery-file-prompt" });
2769
+ const supplied = await input.promptForRecoveryFile();
2770
+ if (!supplied) throw new NewDeviceError();
2771
+ const file = parseRecoveryShareFile(supplied.value);
2772
+ input.onProgress?.({ type: "password-prompt", passwordHint: file.passwordHint });
2773
+ const password = await supplied.getPassword();
2774
+ if (!password) throw new NewDeviceError();
2775
+ const recoveryShare = await decryptRecoveryShareFile(file, password);
2776
+ input.onProgress?.({ type: "recovery-verifying" });
2480
2777
  const tee = new TeeClient(input.teeUrl);
2481
- const verified = await tee.verifyRecoveryCandidate(candidate, input.jwt);
2482
- if (!verified) {
2483
- zeroize(candidate);
2484
- throw new RecoveryFailedError("phrase-mismatch");
2778
+ let result;
2779
+ try {
2780
+ result = await tee.recoverWithShare(input.jwt, recoveryShare);
2781
+ } finally {
2782
+ zeroize(recoveryShare);
2485
2783
  }
2486
- return candidate;
2784
+ input.onProgress?.({ type: "rotated", shareSetVersion: result.shareSetVersion });
2785
+ await input.onNewRecoveryShare?.(result.recoveryShare, result.shareSetVersion);
2786
+ zeroize(result.recoveryShare);
2787
+ return result.deviceShare;
2487
2788
  }
2488
2789
 
2489
- // src/social/recovery/phraseUi.ts
2490
- function buildPhraseResult(deviceShare) {
2491
- const phrase = shareToPhrase(deviceShare);
2492
- return { phrase, challenge: phraseConfirmationChallenge(phrase) };
2790
+ // src/social/recovery/portableBackup.ts
2791
+ init_errors();
2792
+ var PBKDF2_ITERATIONS3 = 6e5;
2793
+ var FILE_VERSION2 = 1;
2794
+ function b643(u) {
2795
+ let s = "";
2796
+ for (const b of u) s += String.fromCharCode(b);
2797
+ return btoa(s);
2493
2798
  }
2494
- function verifyConfirmationChallenge(challenge, userAnswers) {
2495
- if (userAnswers.length !== challenge.length) return false;
2496
- return challenge.every(({ position, expected }) => {
2497
- const answer = userAnswers.find((a) => a.position === position);
2498
- return !!answer && answer.word.trim().toLowerCase() === expected.toLowerCase();
2499
- });
2799
+ function buf5(u) {
2800
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2801
+ }
2802
+ async function deriveFileKey2(password, salt, iterations) {
2803
+ const base = await crypto.subtle.importKey(
2804
+ "raw",
2805
+ new TextEncoder().encode(password),
2806
+ "PBKDF2",
2807
+ false,
2808
+ ["deriveKey"]
2809
+ );
2810
+ return crypto.subtle.deriveKey(
2811
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
2812
+ base,
2813
+ { name: "AES-GCM", length: 256 },
2814
+ false,
2815
+ ["encrypt", "decrypt"]
2816
+ );
2817
+ }
2818
+ async function buildPortableBackup(deviceShare, recoveryShare, password, addresses, shareSetVersion, passwordHint) {
2819
+ if (deviceShare.length !== 33 || recoveryShare.length !== 33) {
2820
+ throw new Error("shares must be 33 bytes");
2821
+ }
2822
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2823
+ if (passwordHint && passwordHint.trim() === password) {
2824
+ throw new Error("password hint must not be the password");
2825
+ }
2826
+ const plaintext = new TextEncoder().encode(
2827
+ JSON.stringify({ deviceShare: b643(deviceShare), recoveryShare: b643(recoveryShare) })
2828
+ );
2829
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2830
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2831
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
2832
+ const ct = new Uint8Array(
2833
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
2834
+ );
2835
+ return {
2836
+ version: FILE_VERSION2,
2837
+ type: "decane-portable-backup",
2838
+ shareSetVersion,
2839
+ kdf: "pbkdf2-sha256",
2840
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
2841
+ cipher: "aes-256-gcm",
2842
+ iv: b643(iv),
2843
+ ciphertext: b643(ct),
2844
+ addresses,
2845
+ passwordHint: passwordHint?.trim() || null,
2846
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2847
+ };
2500
2848
  }
2501
2849
 
2502
2850
  // src/social/client.ts
@@ -2568,6 +2916,18 @@ async function createDecaneConnect(config) {
2568
2916
  }
2569
2917
  };
2570
2918
  }
2919
+ async function persistRotatedDeviceShare(userId, deviceShare) {
2920
+ const stored = await storage.getShare(userId);
2921
+ if (!stored) {
2922
+ throw new CallSequenceError(
2923
+ "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
2924
+ );
2925
+ }
2926
+ const wrappingKey = await getWrappingKey(stored);
2927
+ const iv = generateRandomBytes(12);
2928
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
2929
+ await storage.putShare({ ...stored, wrappedDeviceShare, iv });
2930
+ }
2571
2931
  async function getWrappingKey(stored) {
2572
2932
  if (stored.unlockMethod === "passkey") {
2573
2933
  const prfOutput = await getPrfOutput({
@@ -2587,15 +2947,7 @@ async function createDecaneConnect(config) {
2587
2947
  let credentialId;
2588
2948
  let prfOutput;
2589
2949
  try {
2590
- const authType = config.authMethods[0] ?? "social";
2591
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2592
- const result = await registerPasskey({
2593
- rpId,
2594
- rpName,
2595
- userId: new TextEncoder().encode(userId),
2596
- userName: `decane-wallet-${authType}-${dateStr}`,
2597
- prfSalt
2598
- });
2950
+ const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2599
2951
  credentialId = result.credentialId;
2600
2952
  prfOutput = result.prfOutput;
2601
2953
  } catch (e) {
@@ -2634,15 +2986,7 @@ async function createDecaneConnect(config) {
2634
2986
  if (isPasskeySupported()) {
2635
2987
  const prfSalt = generateRandomBytes(32);
2636
2988
  try {
2637
- const authType = config.authMethods[0] ?? "social";
2638
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2639
- const { credentialId, prfOutput } = await registerPasskey({
2640
- rpId,
2641
- rpName,
2642
- userId: new TextEncoder().encode(userId),
2643
- userName: `decane-wallet-${authType}-${dateStr}`,
2644
- prfSalt
2645
- });
2989
+ const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2646
2990
  if (prfOutput) {
2647
2991
  unlockMethod = "passkey";
2648
2992
  wrappingKey = await importAesKey(prfOutput);
@@ -2665,19 +3009,21 @@ async function createDecaneConnect(config) {
2665
3009
  ({ wrappingKey, meta } = await pinSetup());
2666
3010
  unlockMethod = "pin";
2667
3011
  }
2668
- const { privateKey, deviceShare, serverShare } = generateWalletShares();
3012
+ const master = generateMaster();
3013
+ let shareSet;
2669
3014
  let evmAddress;
2670
3015
  let solanaAddress;
2671
3016
  try {
2672
- evmAddress = new import_ethers2.Wallet(toHex(privateKey)).address;
2673
- solanaAddress = import_web32.Keypair.fromSeed(privateKey).publicKey.toBase58();
3017
+ evmAddress = new import_ethers2.Wallet(toHex(master)).address;
3018
+ solanaAddress = import_web32.Keypair.fromSeed(master).publicKey.toBase58();
3019
+ shareSet = await splitMaster(master, 1);
2674
3020
  } finally {
2675
- zeroize(privateKey);
3021
+ zeroize(master);
2676
3022
  }
2677
3023
  const iv = generateRandomBytes(12);
2678
3024
  const wrappedDeviceShare = await aesGcmEncrypt(
2679
3025
  wrappingKey,
2680
- deviceShare,
3026
+ shareSet.device,
2681
3027
  iv
2682
3028
  );
2683
3029
  await storage.putShare({
@@ -2693,7 +3039,7 @@ async function createDecaneConnect(config) {
2693
3039
  });
2694
3040
  const serverIv = generateRandomBytes(12);
2695
3041
  await api.putShare(jwt, {
2696
- ciphertext: serverShare,
3042
+ ciphertext: shareSet.server,
2697
3043
  iv: serverIv,
2698
3044
  wrappedShareMeta: {
2699
3045
  unlockMethod
@@ -2702,7 +3048,7 @@ async function createDecaneConnect(config) {
2702
3048
  evmAddress,
2703
3049
  solanaAddress
2704
3050
  });
2705
- zeroize(serverShare);
3051
+ zeroize(shareSet.server);
2706
3052
  const addresses = { evm: evmAddress, solana: solanaAddress };
2707
3053
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
2708
3054
  try {
@@ -2717,32 +3063,47 @@ async function createDecaneConnect(config) {
2717
3063
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
2718
3064
  }
2719
3065
  }
2720
- await session.start(deviceShare, jwt, addresses);
2721
- if (config.offerPhraseAtSignup !== false && config.onPhraseGenerated) {
3066
+ const confirmed = await session.start(shareSet.device, jwt, addresses);
3067
+ zeroize(shareSet.device);
3068
+ if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3069
+ console.warn(
3070
+ `decane-connect-kit: signer derived a different EVM address (${confirmed.evm}) than this device did (${addresses.evm}) during setup. Using the signer's.`
3071
+ );
3072
+ }
3073
+ addresses.evm = evmAddress = confirmed.evm;
3074
+ addresses.solana = solanaAddress = confirmed.solana;
3075
+ if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
2722
3076
  try {
2723
- const result = buildPhraseResult(deviceShare);
2724
- const { userConfirmed, userAnswers } = await config.onPhraseGenerated(result);
2725
- if (userConfirmed && verifyConfirmationChallenge(result.challenge, userAnswers)) {
2726
- await api.markPhraseSaved(jwt);
3077
+ const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3078
+ if (choice.wants) {
3079
+ const file = await buildRecoveryShareFile(
3080
+ shareSet.recovery,
3081
+ choice.password,
3082
+ addresses,
3083
+ shareSet.version,
3084
+ choice.passwordHint
3085
+ );
3086
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
3087
+ await api.markRecoveryShareSaved(jwt);
2727
3088
  }
2728
3089
  } catch (e) {
2729
- console.warn("decane-connect-kit: recovery phrase offer failed or was skipped.", e);
3090
+ console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
2730
3091
  }
2731
3092
  }
2732
- zeroize(deviceShare);
3093
+ zeroize(shareSet.recovery);
2733
3094
  return addresses;
2734
3095
  }
2735
3096
  async function returningUserUnlock(userId, jwt) {
2736
3097
  const rawAddresses = await api.getAddresses(jwt);
2737
- if (!rawAddresses.evm || !rawAddresses.solana) {
2738
- throw new CallSequenceError(
2739
- "No addresses are recorded for this user \u2014 inconsistent server state"
2740
- );
2741
- }
2742
- const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
3098
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
2743
3099
  const stored = await storage.getShare(userId);
2744
3100
  let deviceShare;
2745
3101
  if (!stored) {
3102
+ if (!config.onRecoveryRotated) {
3103
+ throw new CallSequenceError(
3104
+ "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."
3105
+ );
3106
+ }
2746
3107
  deviceShare = await recoverDeviceShare({
2747
3108
  userId,
2748
3109
  jwt,
@@ -2751,7 +3112,15 @@ async function createDecaneConnect(config) {
2751
3112
  api,
2752
3113
  storage,
2753
3114
  onProgress: config.onRecoveryProgress,
2754
- promptForPhrase: config.promptForRecoveryPhrase
3115
+ promptForRecoveryFile: config.promptForRecoveryFile,
3116
+ onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
3117
+ if (!config.onRecoveryRotated) return;
3118
+ if (!recordedAddresses) return;
3119
+ const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: recordedAddresses.evm, solanaAddress: recordedAddresses.solana });
3120
+ const file = await buildRecoveryShareFile(recoveryShare, password, recordedAddresses, shareSetVersion, passwordHint);
3121
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(recordedAddresses.evm));
3122
+ await api.markRecoveryShareSaved(jwt);
3123
+ }
2755
3124
  });
2756
3125
  if (!await storage.getShare(userId) && isPasskeySupported()) {
2757
3126
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
@@ -2768,8 +3137,13 @@ async function createDecaneConnect(config) {
2768
3137
  throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
2769
3138
  }
2770
3139
  }
2771
- await session.start(deviceShare, jwt, addresses);
3140
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
2772
3141
  zeroize(deviceShare);
3142
+ if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
3143
+ console.warn(
3144
+ `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.`
3145
+ );
3146
+ }
2773
3147
  return addresses;
2774
3148
  }
2775
3149
  async function handleAuthResult(result) {
@@ -2831,14 +3205,24 @@ async function createDecaneConnect(config) {
2831
3205
  return handleAuthResult(result);
2832
3206
  },
2833
3207
  async unlock() {
2834
- if (!auth)
2835
- throw new CallSequenceError("Not connected \u2014 call connect() first");
3208
+ if (!auth) {
3209
+ throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
3210
+ }
2836
3211
  if (session.isActive()) return;
2837
3212
  await returningUserUnlock(auth.userId, auth.jwt);
2838
3213
  },
2839
3214
  isUnlocked() {
2840
3215
  return session.isActive();
2841
3216
  },
3217
+ /**
3218
+ * True when we have a persisted identity but no live auth state, i.e.
3219
+ * unlock() cannot succeed and the user must sign in again. Exposed on the
3220
+ * client (not just the React context) so non-React consumers can branch on
3221
+ * it without having to catch ReconnectRequiredError.
3222
+ */
3223
+ needsReconnect() {
3224
+ return !auth && !!persisted;
3225
+ },
2842
3226
  sessionExpiresAt() {
2843
3227
  return session.expiresAtTimestamp();
2844
3228
  },
@@ -2926,23 +3310,86 @@ async function createDecaneConnect(config) {
2926
3310
  getAddresses() {
2927
3311
  return auth?.addresses ?? persisted?.addresses ?? null;
2928
3312
  },
2929
- async generateRecoveryPhrase() {
3313
+ async rotateShares(opts) {
2930
3314
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2931
- const stored = await storage.getShare(auth.userId);
2932
- if (!stored) throw new NewDeviceError();
2933
- const wrappingKey = await getWrappingKey(stored);
2934
- const deviceShare = await aesGcmDecrypt(wrappingKey, stored.wrappedDeviceShare, stored.iv);
3315
+ const tee = session.requireActive();
3316
+ const result = await tee.rotateShares(auth.jwt);
3317
+ try {
3318
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3319
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3320
+ const file = await buildRecoveryShareFile(
3321
+ result.recoveryShare,
3322
+ opts.password,
3323
+ addresses,
3324
+ result.shareSetVersion,
3325
+ opts.passwordHint
3326
+ );
3327
+ await api.markRecoveryShareSaved(auth.jwt);
3328
+ return file;
3329
+ } finally {
3330
+ zeroize(result.deviceShare);
3331
+ zeroize(result.recoveryShare);
3332
+ }
3333
+ },
3334
+ async exportPortableBackup(opts) {
3335
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3336
+ const tee = session.requireActive();
3337
+ const result = await tee.exportPortableBackup(auth.jwt);
2935
3338
  try {
2936
- return buildPhraseResult(deviceShare);
3339
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3340
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3341
+ return await buildPortableBackup(
3342
+ result.deviceShare,
3343
+ result.recoveryShare,
3344
+ opts.password,
3345
+ addresses,
3346
+ result.shareSetVersion,
3347
+ opts.passwordHint
3348
+ );
2937
3349
  } finally {
2938
- zeroize(deviceShare);
3350
+ zeroize(result.deviceShare);
3351
+ zeroize(result.recoveryShare);
2939
3352
  }
2940
3353
  },
2941
- async markPhraseSaved(challenge, userAnswers) {
3354
+ async hasRecoveryShare() {
2942
3355
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2943
- if (!verifyConfirmationChallenge(challenge, userAnswers)) return false;
2944
- await api.markPhraseSaved(auth.jwt);
2945
- return true;
3356
+ const status = await api.getRecoveryStatus(auth.jwt);
3357
+ return status.hasRecoveryShare;
3358
+ },
3359
+ async shouldPromptRecovery() {
3360
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3361
+ const status = await api.getRecoveryStatus(auth.jwt);
3362
+ if (status.hasRecoveryShare || status.nagged) return false;
3363
+ try {
3364
+ const checks = [];
3365
+ if (auth.addresses.evm) {
3366
+ const { JsonRpcProvider } = await import("ethers");
3367
+ for (const chain of config.chains.filter((c) => c.startsWith("evm:"))) {
3368
+ const rpcUrl = config.rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
3369
+ if (!rpcUrl) continue;
3370
+ checks.push(
3371
+ new JsonRpcProvider(rpcUrl).getBalance(auth.addresses.evm).then((b) => b > 0n).catch(() => false)
3372
+ );
3373
+ }
3374
+ }
3375
+ if (auth.addresses.solana && config.chains.some((c) => c.startsWith("solana:"))) {
3376
+ const { Connection, PublicKey } = await import("@solana/web3.js");
3377
+ const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3378
+ const cluster = solChain.slice("solana:".length);
3379
+ const endpoint = cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
3380
+ checks.push(
3381
+ new Connection(endpoint).getBalance(new PublicKey(auth.addresses.solana)).then((b) => b > 0).catch(() => false)
3382
+ );
3383
+ }
3384
+ const results = await Promise.all(checks);
3385
+ return results.some(Boolean);
3386
+ } catch {
3387
+ return false;
3388
+ }
3389
+ },
3390
+ async dismissRecoveryPrompt() {
3391
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3392
+ await api.dismissRecoveryNag(auth.jwt);
2946
3393
  },
2947
3394
  async hasBackup() {
2948
3395
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
@@ -2957,16 +3404,30 @@ async function createDecaneConnect(config) {
2957
3404
  };
2958
3405
  return client;
2959
3406
  }
2960
- function bufferToB64u(buf4) {
2961
- return btoa(String.fromCharCode(...buf4)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3407
+ function bufferToB64u(buf6) {
3408
+ return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2962
3409
  }
2963
3410
  function b64uToBuffer(b64u) {
2964
- const b642 = b64u.replace(/-/g, "+").replace(/_/g, "/");
2965
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
3411
+ const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3412
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2966
3413
  }
2967
3414
  function toHex(bytes) {
2968
3415
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2969
3416
  }
3417
+ async function deriveAddressesFromShares(a, b) {
3418
+ const master = await combineShares(a, b);
3419
+ try {
3420
+ return {
3421
+ evm: new import_ethers2.Wallet(toHex(master)).address,
3422
+ solana: import_web32.Keypair.fromSeed(master).publicKey.toBase58()
3423
+ };
3424
+ } finally {
3425
+ zeroize(master);
3426
+ }
3427
+ }
3428
+
3429
+ // src/social/index.ts
3430
+ init_errors();
2970
3431
 
2971
3432
  // src/ui/WalletSelector.tsx
2972
3433
  var import_react2 = __toESM(require("react"), 1);
@@ -3889,6 +4350,15 @@ function countdown(expiresAt) {
3889
4350
  const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
3890
4351
  return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
3891
4352
  }
4353
+ function downloadJson(data, filename) {
4354
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
4355
+ const url = URL.createObjectURL(blob);
4356
+ const a = document.createElement("a");
4357
+ a.href = url;
4358
+ a.download = filename;
4359
+ a.click();
4360
+ URL.revokeObjectURL(url);
4361
+ }
3892
4362
  function CopyButton({ value }) {
3893
4363
  const [copied, setCopied] = (0, import_react3.useState)(false);
3894
4364
  async function copy() {
@@ -3916,7 +4386,54 @@ function CopyButton({ value }) {
3916
4386
  copied ? "\u2713" : "copy"
3917
4387
  );
3918
4388
  }
3919
- function AddressRow({ label, address }) {
4389
+ function useBalance(kind, address, chain, rpcUrls) {
4390
+ const [state, setState] = (0, import_react3.useState)({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
4391
+ (0, import_react3.useEffect)(() => {
4392
+ if (!address || !chain) return;
4393
+ let cancelled = false;
4394
+ setState((s) => ({ ...s, loading: true }));
4395
+ (async () => {
4396
+ try {
4397
+ if (kind === "evm") {
4398
+ const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
4399
+ if (!rpcUrl) {
4400
+ if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
4401
+ return;
4402
+ }
4403
+ const { JsonRpcProvider, formatEther } = await import("ethers");
4404
+ const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
4405
+ if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
4406
+ } else {
4407
+ const cluster = chain.slice("solana:".length);
4408
+ const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
4409
+ const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
4410
+ const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
4411
+ if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
4412
+ }
4413
+ } catch {
4414
+ if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
4415
+ }
4416
+ })();
4417
+ return () => {
4418
+ cancelled = true;
4419
+ };
4420
+ }, [kind, address, chain, rpcUrls]);
4421
+ return state;
4422
+ }
4423
+ function formatBalance(value) {
4424
+ if (value == null) return "\u2014";
4425
+ const n = Number(value);
4426
+ if (!Number.isFinite(n)) return value;
4427
+ return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
4428
+ }
4429
+ function AddressRow({
4430
+ label,
4431
+ address,
4432
+ kind,
4433
+ chain,
4434
+ rpcUrls
4435
+ }) {
4436
+ const balance = useBalance(kind, address, chain, rpcUrls);
3920
4437
  const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
3921
4438
  return /* @__PURE__ */ import_react3.default.createElement("div", { style: {
3922
4439
  background: "var(--dc-surface)",
@@ -3924,7 +4441,233 @@ function AddressRow({ label, address }) {
3924
4441
  borderRadius: 10,
3925
4442
  padding: "10px 14px",
3926
4443
  marginBottom: 8
3927
- } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 5 } }, label), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ import_react3.default.createElement(CopyButton, { value: address })));
4444
+ } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 } }, /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase" } }, label), /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, fontFamily: "monospace", color: "var(--dc-text)" } }, balance.loading ? "\u2026" : `${formatBalance(balance.value)} ${balance.symbol}`)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ import_react3.default.createElement(CopyButton, { value: address })));
4445
+ }
4446
+ function SignInView({
4447
+ onGoogleSignIn,
4448
+ onEmailStart,
4449
+ onEmailVerify,
4450
+ authMethods,
4451
+ resumeHint
4452
+ }) {
4453
+ const [googleLoading, setGoogleLoading] = (0, import_react3.useState)(false);
4454
+ const [email, setEmail] = (0, import_react3.useState)("");
4455
+ const [code, setCode] = (0, import_react3.useState)("");
4456
+ const [emailSent, setEmailSent] = (0, import_react3.useState)(false);
4457
+ const [emailLoading, setEmailLoading] = (0, import_react3.useState)(false);
4458
+ const [error, setError] = (0, import_react3.useState)("");
4459
+ async function handleGoogle() {
4460
+ setError("");
4461
+ setGoogleLoading(true);
4462
+ try {
4463
+ await onGoogleSignIn();
4464
+ } catch (e) {
4465
+ setError(e.message);
4466
+ setGoogleLoading(false);
4467
+ }
4468
+ }
4469
+ async function handleEmailStart(e) {
4470
+ e.preventDefault();
4471
+ setError("");
4472
+ setEmailLoading(true);
4473
+ try {
4474
+ await onEmailStart(email);
4475
+ setEmailSent(true);
4476
+ } catch (e2) {
4477
+ setError(e2.message);
4478
+ } finally {
4479
+ setEmailLoading(false);
4480
+ }
4481
+ }
4482
+ async function handleEmailVerify(e) {
4483
+ e.preventDefault();
4484
+ setError("");
4485
+ setEmailLoading(true);
4486
+ try {
4487
+ await onEmailVerify(email, code);
4488
+ } catch (e2) {
4489
+ setError(e2.message);
4490
+ } finally {
4491
+ setEmailLoading(false);
4492
+ }
4493
+ }
4494
+ const inputStyle = {
4495
+ width: "100%",
4496
+ background: "var(--dc-surface)",
4497
+ border: "1px solid var(--dc-border)",
4498
+ borderRadius: 8,
4499
+ color: "var(--dc-text)",
4500
+ padding: "9px 12px",
4501
+ fontSize: 13,
4502
+ outline: "none",
4503
+ fontFamily: "inherit",
4504
+ boxSizing: "border-box"
4505
+ };
4506
+ return /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px 16px" } }, resumeHint && /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement(
4507
+ "button",
4508
+ {
4509
+ onClick: handleGoogle,
4510
+ disabled: googleLoading,
4511
+ style: {
4512
+ width: "100%",
4513
+ padding: "10px 0",
4514
+ marginBottom: 10,
4515
+ background: "var(--dc-accent)",
4516
+ color: "var(--dc-accent-text)",
4517
+ border: "none",
4518
+ borderRadius: 10,
4519
+ fontSize: 13,
4520
+ fontWeight: 700,
4521
+ cursor: googleLoading ? "not-allowed" : "pointer",
4522
+ opacity: googleLoading ? 0.7 : 1,
4523
+ fontFamily: "inherit"
4524
+ }
4525
+ },
4526
+ googleLoading ? "Redirecting\u2026" : "Continue with Google"
4527
+ ), authMethods.includes("email") && /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
4528
+ "input",
4529
+ {
4530
+ type: "email",
4531
+ required: true,
4532
+ placeholder: "your@email.com",
4533
+ value: email,
4534
+ onChange: (e) => setEmail(e.target.value),
4535
+ style: { ...inputStyle, flex: 1 }
4536
+ }
4537
+ ), /* @__PURE__ */ import_react3.default.createElement("button", { type: "submit", disabled: emailLoading, style: {
4538
+ padding: "0 16px",
4539
+ background: "var(--dc-surface)",
4540
+ border: "1px solid var(--dc-border)",
4541
+ borderRadius: 8,
4542
+ color: "var(--dc-text)",
4543
+ fontSize: 13,
4544
+ cursor: "pointer",
4545
+ fontFamily: "inherit"
4546
+ } }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ import_react3.default.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement(
4547
+ "input",
4548
+ {
4549
+ placeholder: "6-digit code",
4550
+ value: code,
4551
+ maxLength: 6,
4552
+ autoFocus: true,
4553
+ onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
4554
+ style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
4555
+ }
4556
+ ), /* @__PURE__ */ import_react3.default.createElement("button", { type: "submit", disabled: emailLoading, style: {
4557
+ padding: "0 16px",
4558
+ background: "var(--dc-accent)",
4559
+ color: "var(--dc-accent-text)",
4560
+ border: "none",
4561
+ borderRadius: 8,
4562
+ fontSize: 13,
4563
+ fontWeight: 700,
4564
+ cursor: "pointer",
4565
+ fontFamily: "inherit"
4566
+ } }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
4567
+ }
4568
+ function RecoverySection({
4569
+ addresses,
4570
+ onRotateShares,
4571
+ onExportPortableBackup
4572
+ }) {
4573
+ const [expanded, setExpanded] = (0, import_react3.useState)(false);
4574
+ const [mode, setMode] = (0, import_react3.useState)(null);
4575
+ const [password, setPassword] = (0, import_react3.useState)("");
4576
+ const [passwordHint, setPasswordHint] = (0, import_react3.useState)("");
4577
+ const [confirmed, setConfirmed] = (0, import_react3.useState)(false);
4578
+ const [busy, setBusy] = (0, import_react3.useState)(false);
4579
+ const [result, setResult] = (0, import_react3.useState)(null);
4580
+ const [error, setError] = (0, import_react3.useState)("");
4581
+ function reset() {
4582
+ setMode(null);
4583
+ setPassword("");
4584
+ setPasswordHint("");
4585
+ setConfirmed(false);
4586
+ setError("");
4587
+ }
4588
+ async function handleGenerate() {
4589
+ if (!addresses?.evm || !mode) return;
4590
+ if (password.length < 8) {
4591
+ setError("Password must be at least 8 characters.");
4592
+ return;
4593
+ }
4594
+ if (mode === "portable" && !confirmed) {
4595
+ setError("Confirm you understand the risk first.");
4596
+ return;
4597
+ }
4598
+ setBusy(true);
4599
+ setError("");
4600
+ try {
4601
+ if (mode === "share") {
4602
+ const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
4603
+ downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
4604
+ setResult({ kind: "share", version: file.shareSetVersion });
4605
+ } else {
4606
+ const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
4607
+ downloadJson(file, `decane-PORTABLE-BACKUP-${addresses.evm.slice(0, 8)}.json`);
4608
+ setResult({ kind: "portable", version: file.shareSetVersion });
4609
+ }
4610
+ reset();
4611
+ } catch (e) {
4612
+ setError(e.message);
4613
+ } finally {
4614
+ setBusy(false);
4615
+ }
4616
+ }
4617
+ const inputStyle = {
4618
+ width: "100%",
4619
+ background: "var(--dc-bg)",
4620
+ border: "1px solid var(--dc-border)",
4621
+ borderRadius: 8,
4622
+ color: "var(--dc-text)",
4623
+ padding: "8px 10px",
4624
+ fontSize: 12,
4625
+ outline: "none",
4626
+ fontFamily: "inherit",
4627
+ boxSizing: "border-box"
4628
+ };
4629
+ const btnStyle = {
4630
+ padding: "8px 12px",
4631
+ borderRadius: 8,
4632
+ fontSize: 12,
4633
+ fontWeight: 700,
4634
+ cursor: "pointer",
4635
+ fontFamily: "inherit",
4636
+ border: "none"
4637
+ };
4638
+ return /* @__PURE__ */ import_react3.default.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ import_react3.default.createElement(
4639
+ "button",
4640
+ {
4641
+ onClick: () => setExpanded((v) => !v),
4642
+ style: {
4643
+ width: "100%",
4644
+ padding: "10px 14px",
4645
+ background: "var(--dc-surface)",
4646
+ border: "none",
4647
+ display: "flex",
4648
+ alignItems: "center",
4649
+ justifyContent: "space-between",
4650
+ cursor: "pointer",
4651
+ fontFamily: "inherit"
4652
+ }
4653
+ },
4654
+ /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
4655
+ /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
4656
+ ), expanded && /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "12px 14px", borderTop: "1px solid var(--dc-border)" } }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => {
4657
+ reset();
4658
+ setMode("share");
4659
+ }, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => {
4660
+ reset();
4661
+ setMode("portable");
4662
+ }, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate portable backup (Decane-is-down)")), mode === "share" && /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ import_react3.default.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), mode === "portable" && /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4663
+ background: "rgba(255,85,51,0.08)",
4664
+ border: "1px solid rgba(255,85,51,0.3)",
4665
+ borderRadius: 8,
4666
+ padding: "8px 10px",
4667
+ fontSize: 11,
4668
+ color: "#ff9977",
4669
+ lineHeight: 1.5
4670
+ } }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--dc-text)" } }, /* @__PURE__ */ import_react3.default.createElement("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), style: { width: "auto" } }), "I understand this is a bearer secret"), /* @__PURE__ */ import_react3.default.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ import_react3.default.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ import_react3.default.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ import_react3.default.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__ */ import_react3.default.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), result && /* @__PURE__ */ import_react3.default.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.")));
3928
4671
  }
3929
4672
  function SocialWalletModal({
3930
4673
  open,
@@ -3932,7 +4675,15 @@ function SocialWalletModal({
3932
4675
  theme = "auto",
3933
4676
  socialAuth,
3934
4677
  onUnlock,
3935
- onDisconnect
4678
+ onDisconnect,
4679
+ onGoogleSignIn,
4680
+ onEmailStart,
4681
+ onEmailVerify,
4682
+ authMethods = ["google", "email"],
4683
+ onRotateShares,
4684
+ onExportPortableBackup,
4685
+ rpcUrls,
4686
+ chains
3936
4687
  }) {
3937
4688
  const [timer, setTimer] = (0, import_react3.useState)("");
3938
4689
  const [unlocking, setUnlocking] = (0, import_react3.useState)(false);
@@ -3981,6 +4732,9 @@ function SocialWalletModal({
3981
4732
  setDisconnecting(false);
3982
4733
  }
3983
4734
  }
4735
+ const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
4736
+ const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
4737
+ const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
3984
4738
  const modal = /* @__PURE__ */ import_react3.default.createElement(
3985
4739
  "div",
3986
4740
  {
@@ -4018,10 +4772,14 @@ function SocialWalletModal({
4018
4772
  borderRadius: 20,
4019
4773
  width: "100%",
4020
4774
  maxWidth: 380,
4021
- boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
4022
- overflow: "hidden"
4775
+ maxHeight: "90vh",
4776
+ overflowY: "auto",
4777
+ boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
4023
4778
  } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4024
4779
  height: 2,
4780
+ position: "sticky",
4781
+ top: 0,
4782
+ zIndex: 1,
4025
4783
  background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
4026
4784
  } }), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4027
4785
  width: 34,
@@ -4050,7 +4808,7 @@ function SocialWalletModal({
4050
4808
  strokeLinecap: "round",
4051
4809
  strokeLinejoin: "round"
4052
4810
  }
4053
- ))), /* @__PURE__ */ import_react3.default.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split between device & server")), /* @__PURE__ */ import_react3.default.createElement(
4811
+ ))), /* @__PURE__ */ import_react3.default.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ import_react3.default.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split three ways")), /* @__PURE__ */ import_react3.default.createElement(
4054
4812
  "button",
4055
4813
  {
4056
4814
  onClick: onClose,
@@ -4070,7 +4828,16 @@ function SocialWalletModal({
4070
4828
  }
4071
4829
  },
4072
4830
  /* @__PURE__ */ import_react3.default.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ import_react3.default.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
4073
- )), /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4831
+ )), showSignIn ? /* @__PURE__ */ import_react3.default.createElement(
4832
+ SignInView,
4833
+ {
4834
+ onGoogleSignIn,
4835
+ onEmailStart,
4836
+ onEmailVerify,
4837
+ authMethods,
4838
+ resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
4839
+ }
4840
+ ) : /* @__PURE__ */ import_react3.default.createElement(import_react3.default.Fragment, null, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4074
4841
  margin: "0 16px 14px",
4075
4842
  padding: "8px 14px",
4076
4843
  background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
@@ -4090,7 +4857,14 @@ function SocialWalletModal({
4090
4857
  fontSize: 12,
4091
4858
  fontWeight: 700,
4092
4859
  color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
4093
- } }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px 16px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm }), socialAuth.addresses?.solana && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana }), !socialAuth.addresses?.evm && !socialAuth.addresses?.solana && /* @__PURE__ */ import_react3.default.createElement("div", { style: { textAlign: "center", padding: "16px 0", color: "var(--dc-text-muted)", fontSize: 13 } }, "Unlock wallet to reveal addresses")), /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4860
+ } }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ import_react3.default.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ import_react3.default.createElement("div", { style: { padding: "0 16px 14px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm, kind: "evm", chain: evmChain, rpcUrls }), socialAuth.addresses?.solana && /* @__PURE__ */ import_react3.default.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana, kind: "solana", chain: solChain, rpcUrls })), socialAuth.isUnlocked && /* @__PURE__ */ import_react3.default.createElement(
4861
+ RecoverySection,
4862
+ {
4863
+ addresses: socialAuth.addresses,
4864
+ onRotateShares,
4865
+ onExportPortableBackup
4866
+ }
4867
+ ), /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4094
4868
  padding: "14px 16px",
4095
4869
  borderTop: "1px solid var(--dc-border)",
4096
4870
  display: "flex",
@@ -4135,7 +4909,7 @@ function SocialWalletModal({
4135
4909
  }
4136
4910
  },
4137
4911
  disconnecting ? "Signing out\u2026" : "Sign out"
4138
- )))
4912
+ ))))
4139
4913
  );
4140
4914
  return (0, import_react_dom2.createPortal)(modal, document.body);
4141
4915
  }
@@ -4146,7 +4920,8 @@ var DEFAULT_SOCIAL = {
4146
4920
  addresses: null,
4147
4921
  isNewUser: false,
4148
4922
  isUnlocked: false,
4149
- sessionExpiresAt: null
4923
+ sessionExpiresAt: null,
4924
+ needsReconnect: false
4150
4925
  };
4151
4926
  var DecaneContext = (0, import_react4.createContext)(null);
4152
4927
  function hasSocial(mode) {
@@ -4206,7 +4981,7 @@ function DecaneKit({ config, children }) {
4206
4981
  sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
4207
4982
  const saved = sdk.getAddresses();
4208
4983
  if (saved) {
4209
- setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
4984
+ setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4210
4985
  }
4211
4986
  const params = new URLSearchParams(window.location.search);
4212
4987
  if (params.has("decane_jwt") || params.has("decane_error")) {
@@ -4220,7 +4995,8 @@ function DecaneKit({ config, children }) {
4220
4995
  addresses: result.addresses,
4221
4996
  isNewUser: result.isNewUser,
4222
4997
  isUnlocked: sdk?.isUnlocked() ?? false,
4223
- sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
4998
+ sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4999
+ needsReconnect: false
4224
5000
  });
4225
5001
  }
4226
5002
  const connectWithGoogle = (0, import_react4.useCallback)(async () => {
@@ -4247,7 +5023,8 @@ function DecaneKit({ config, children }) {
4247
5023
  setSocialAuth((p) => ({
4248
5024
  ...p,
4249
5025
  isUnlocked: sdk.isUnlocked(),
4250
- sessionExpiresAt: sdk.sessionExpiresAt()
5026
+ sessionExpiresAt: sdk.sessionExpiresAt(),
5027
+ needsReconnect: false
4251
5028
  }));
4252
5029
  }, []);
4253
5030
  const socialDisconnect = (0, import_react4.useCallback)(async () => {
@@ -4271,6 +5048,31 @@ function DecaneKit({ config, children }) {
4271
5048
  if (!sdk) throw new Error("Social wallet not connected");
4272
5049
  return sdk.signSolanaTransaction(tx);
4273
5050
  }, []);
5051
+ const socialRotateShares = (0, import_react4.useCallback)(async (opts) => {
5052
+ const sdk = socialRef.current;
5053
+ if (!sdk) throw new Error("Social wallet not connected");
5054
+ return sdk.rotateShares(opts);
5055
+ }, []);
5056
+ const socialExportPortableBackup = (0, import_react4.useCallback)(async (opts) => {
5057
+ const sdk = socialRef.current;
5058
+ if (!sdk) throw new Error("Social wallet not connected");
5059
+ return sdk.exportPortableBackup(opts);
5060
+ }, []);
5061
+ const socialHasRecoveryShare = (0, import_react4.useCallback)(async () => {
5062
+ const sdk = socialRef.current;
5063
+ if (!sdk) throw new Error("Social wallet not connected");
5064
+ return sdk.hasRecoveryShare();
5065
+ }, []);
5066
+ const socialShouldPromptRecovery = (0, import_react4.useCallback)(async () => {
5067
+ const sdk = socialRef.current;
5068
+ if (!sdk) throw new Error("Social wallet not connected");
5069
+ return sdk.shouldPromptRecovery();
5070
+ }, []);
5071
+ const socialDismissRecoveryPrompt = (0, import_react4.useCallback)(async () => {
5072
+ const sdk = socialRef.current;
5073
+ if (!sdk) throw new Error("Social wallet not connected");
5074
+ return sdk.dismissRecoveryPrompt();
5075
+ }, []);
4274
5076
  const handleWalletConnect = (0, import_react4.useCallback)(
4275
5077
  (wallet, preference) => {
4276
5078
  walletRef.current.connect(wallet, preference).catch(console.error);
@@ -4298,6 +5100,11 @@ function DecaneKit({ config, children }) {
4298
5100
  socialSignMessage,
4299
5101
  socialSendTransaction,
4300
5102
  socialSignSolanaTransaction,
5103
+ socialRotateShares,
5104
+ socialExportPortableBackup,
5105
+ socialHasRecoveryShare,
5106
+ socialShouldPromptRecovery,
5107
+ socialDismissRecoveryPrompt,
4301
5108
  socialModalOpen,
4302
5109
  openSocialModal,
4303
5110
  closeSocialModal
@@ -4328,7 +5135,15 @@ function DecaneKit({ config, children }) {
4328
5135
  theme,
4329
5136
  socialAuth,
4330
5137
  onUnlock: socialUnlock,
4331
- onDisconnect: socialDisconnect
5138
+ onDisconnect: socialDisconnect,
5139
+ onGoogleSignIn: connectWithGoogle,
5140
+ onEmailStart: connectWithEmail,
5141
+ onEmailVerify: verifyEmailCode2,
5142
+ authMethods: config?.social?.authMethods ?? ["google", "email"],
5143
+ onRotateShares: socialRotateShares,
5144
+ onExportPortableBackup: socialExportPortableBackup,
5145
+ rpcUrls: config?.social?.rpcUrls,
5146
+ chains: config?.social?.chains
4332
5147
  }
4333
5148
  )
4334
5149
  );
@@ -4416,6 +5231,11 @@ function useSocialAuth() {
4416
5231
  isNewUser: socialAuth.isNewUser,
4417
5232
  addresses: socialAuth.addresses,
4418
5233
  sessionExpiresAt: socialAuth.sessionExpiresAt,
5234
+ // True when addresses are known (from a previous visit) but there's no
5235
+ // live session — unlock() will always throw in this state (every
5236
+ // unlock requires a fresh JWT, and JWTs are never persisted). Route
5237
+ // this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
5238
+ needsReconnect: socialAuth.needsReconnect,
4419
5239
  googleLoading,
4420
5240
  emailLoading,
4421
5241
  error,
@@ -4435,6 +5255,11 @@ function useSocialWallet() {
4435
5255
  socialSignMessage,
4436
5256
  socialSendTransaction,
4437
5257
  socialSignSolanaTransaction,
5258
+ socialRotateShares,
5259
+ socialExportPortableBackup,
5260
+ socialHasRecoveryShare,
5261
+ socialShouldPromptRecovery,
5262
+ socialDismissRecoveryPrompt,
4438
5263
  openSocialModal,
4439
5264
  closeSocialModal,
4440
5265
  socialModalOpen
@@ -4484,6 +5309,26 @@ function useSocialWallet() {
4484
5309
  throw e;
4485
5310
  }
4486
5311
  }, [socialDisconnect]);
5312
+ const rotateShares = (0, import_react5.useCallback)(
5313
+ (opts) => run(() => socialRotateShares(opts)),
5314
+ [socialRotateShares]
5315
+ );
5316
+ const exportPortableBackup = (0, import_react5.useCallback)(
5317
+ (opts) => run(() => socialExportPortableBackup(opts)),
5318
+ [socialExportPortableBackup]
5319
+ );
5320
+ const hasRecoveryShare = (0, import_react5.useCallback)(
5321
+ () => run(() => socialHasRecoveryShare()),
5322
+ [socialHasRecoveryShare]
5323
+ );
5324
+ const shouldPromptRecovery = (0, import_react5.useCallback)(
5325
+ () => run(() => socialShouldPromptRecovery()),
5326
+ [socialShouldPromptRecovery]
5327
+ );
5328
+ const dismissRecoveryPrompt = (0, import_react5.useCallback)(
5329
+ () => run(() => socialDismissRecoveryPrompt()),
5330
+ [socialDismissRecoveryPrompt]
5331
+ );
4487
5332
  return {
4488
5333
  // State
4489
5334
  isConnected: !!socialAuth.addresses,
@@ -4491,6 +5336,11 @@ function useSocialWallet() {
4491
5336
  isNewUser: socialAuth.isNewUser,
4492
5337
  addresses: socialAuth.addresses,
4493
5338
  sessionExpiresAt: socialAuth.sessionExpiresAt,
5339
+ // True when addresses are known (from a previous visit) but there's no
5340
+ // live session — unlock() will always throw in this state (every
5341
+ // unlock requires a fresh JWT, and JWTs are never persisted). Route
5342
+ // this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
5343
+ needsReconnect: socialAuth.needsReconnect,
4494
5344
  // Modal
4495
5345
  modalOpen: socialModalOpen,
4496
5346
  openModal: openSocialModal,
@@ -4502,6 +5352,12 @@ function useSocialWallet() {
4502
5352
  signMessage,
4503
5353
  sendTransaction,
4504
5354
  signSolanaTransaction: signSolanaTransaction2,
5355
+ // Recovery (2-of-3 Shamir share set)
5356
+ rotateShares,
5357
+ exportPortableBackup,
5358
+ hasRecoveryShare,
5359
+ shouldPromptRecovery,
5360
+ dismissRecoveryPrompt,
4505
5361
  loading,
4506
5362
  error,
4507
5363
  clearError: () => setError(null)
@@ -4740,14 +5596,17 @@ function useSignTypedData() {
4740
5596
  DecaneError,
4741
5597
  DecaneErrorCode,
4742
5598
  DecaneKit,
5599
+ ExportNotAuthorizedError,
4743
5600
  InvalidApiKeyError,
4744
- InvalidPhraseError,
4745
5601
  InvalidPinError,
5602
+ InvalidRecoveryFileError,
4746
5603
  NetworkError,
4747
5604
  NewDeviceError,
4748
5605
  OriginNotAllowedError,
4749
5606
  PromptPinNotConfiguredError,
5607
+ ReconnectRequiredError,
4750
5608
  RecoveryFailedError,
5609
+ RecoveryWalletMismatchError,
4751
5610
  SessionExpiredError,
4752
5611
  ShareCorruptedError,
4753
5612
  SocialWalletModal,
@@ -4755,6 +5614,7 @@ function useSignTypedData() {
4755
5614
  UserCancelledError,
4756
5615
  WalletSelector,
4757
5616
  WebAuthnNotSupportedError,
5617
+ WrongPasswordError,
4758
5618
  createDecane,
4759
5619
  createDecaneConnect,
4760
5620
  startEmailAuth,