decane-connect-kit 2.0.0 → 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
@@ -22,6 +22,7 @@ __export(errors_exports, {
22
22
  NewDeviceError: () => NewDeviceError,
23
23
  OriginNotAllowedError: () => OriginNotAllowedError,
24
24
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
25
+ ReconnectRequiredError: () => ReconnectRequiredError,
25
26
  RecoveryFailedError: () => RecoveryFailedError,
26
27
  RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
27
28
  SessionExpiredError: () => SessionExpiredError,
@@ -31,7 +32,7 @@ __export(errors_exports, {
31
32
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
32
33
  WrongPasswordError: () => WrongPasswordError
33
34
  });
34
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
35
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
35
36
  var init_errors = __esm({
36
37
  "src/social/errors.ts"() {
37
38
  "use strict";
@@ -63,6 +64,15 @@ var init_errors = __esm({
63
64
  super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
64
65
  }
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
+ };
66
76
  UserCancelledError = class extends DecaneConnectError {
67
77
  constructor() {
68
78
  super("USER_CANCELLED", "User cancelled the operation", true);
@@ -2162,6 +2172,13 @@ var TeeClient = class {
2162
2172
  await verifyTeeAttestation(this.teeUrl, nonceHex);
2163
2173
  this.attested = true;
2164
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
+ */
2165
2182
  async openSession(deviceShare, jwt) {
2166
2183
  await this.ensureAttested();
2167
2184
  const res = await fetch(`${this.teeUrl}/signer/session`, {
@@ -2170,7 +2187,9 @@ var TeeClient = class {
2170
2187
  body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2171
2188
  });
2172
2189
  if (!res.ok) throw new Error("session open failed");
2173
- this.handle = (await res.json()).handle;
2190
+ const body = await res.json();
2191
+ this.handle = body.handle;
2192
+ return body.addresses ?? null;
2174
2193
  }
2175
2194
  /**
2176
2195
  * Flow B: exchange a recovery share (from a recovery-share file) for a
@@ -2303,9 +2322,24 @@ var SessionManager = class {
2303
2322
  window.addEventListener("pagehide", () => this.end());
2304
2323
  }
2305
2324
  }
2306
- 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) {
2307
2334
  this.tee = new TeeClient(this.teeUrl);
2308
- 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
+ }
2309
2343
  this.evmAddress = addresses.evm;
2310
2344
  this.solanaAddress = addresses.solana;
2311
2345
  this.expiresAt = Date.now() + this.durationMs;
@@ -2315,6 +2349,7 @@ var SessionManager = class {
2315
2349
  this.events.emit("session-expired");
2316
2350
  }, this.durationMs);
2317
2351
  this.events.emit("session-started");
2352
+ return addresses;
2318
2353
  }
2319
2354
  end() {
2320
2355
  void this.tee?.closeSession();
@@ -2365,6 +2400,11 @@ async function splitMaster(master, version = 1) {
2365
2400
  if (parts.length !== 3) throw new Error("unexpected share count");
2366
2401
  return { device: parts[0], server: parts[1], recovery: parts[2], version };
2367
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
+ }
2368
2408
 
2369
2409
  // src/social/crypto/wrap.ts
2370
2410
  function buf(u) {
@@ -2809,6 +2849,18 @@ async function createDecaneConnect(config) {
2809
2849
  }
2810
2850
  };
2811
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
+ }
2812
2864
  async function getWrappingKey(stored) {
2813
2865
  if (stored.unlockMethod === "passkey") {
2814
2866
  const prfOutput = await getPrfOutput({
@@ -2944,8 +2996,15 @@ async function createDecaneConnect(config) {
2944
2996
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
2945
2997
  }
2946
2998
  }
2947
- await session.start(shareSet.device, jwt, addresses);
2999
+ const confirmed = await session.start(shareSet.device, jwt, addresses);
2948
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;
2949
3008
  if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
2950
3009
  try {
2951
3010
  const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
@@ -2969,15 +3028,15 @@ async function createDecaneConnect(config) {
2969
3028
  }
2970
3029
  async function returningUserUnlock(userId, jwt) {
2971
3030
  const rawAddresses = await api.getAddresses(jwt);
2972
- if (!rawAddresses.evm || !rawAddresses.solana) {
2973
- throw new CallSequenceError(
2974
- "No addresses are recorded for this user \u2014 inconsistent server state"
2975
- );
2976
- }
2977
- const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
3031
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
2978
3032
  const stored = await storage.getShare(userId);
2979
3033
  let deviceShare;
2980
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
+ }
2981
3040
  deviceShare = await recoverDeviceShare({
2982
3041
  userId,
2983
3042
  jwt,
@@ -2988,13 +3047,11 @@ async function createDecaneConnect(config) {
2988
3047
  onProgress: config.onRecoveryProgress,
2989
3048
  promptForRecoveryFile: config.promptForRecoveryFile,
2990
3049
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
2991
- if (!config.onRecoveryRotated) {
2992
- console.warn("decane-connect-kit: share set rotated after recovery, but no onRecoveryRotated callback is configured \u2014 the user has no way to save a new recovery-share file.");
2993
- return;
2994
- }
2995
- const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: addresses.evm, solanaAddress: addresses.solana });
2996
- const file = await buildRecoveryShareFile(recoveryShare, password, addresses, shareSetVersion, passwordHint);
2997
- await config.onRecoveryFileReady?.(file, recoveryShareFileName(addresses.evm));
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));
2998
3055
  await api.markRecoveryShareSaved(jwt);
2999
3056
  }
3000
3057
  });
@@ -3013,8 +3070,13 @@ async function createDecaneConnect(config) {
3013
3070
  throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
3014
3071
  }
3015
3072
  }
3016
- await session.start(deviceShare, jwt, addresses);
3073
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
3017
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
+ }
3018
3080
  return addresses;
3019
3081
  }
3020
3082
  async function handleAuthResult(result) {
@@ -3076,14 +3138,24 @@ async function createDecaneConnect(config) {
3076
3138
  return handleAuthResult(result);
3077
3139
  },
3078
3140
  async unlock() {
3079
- if (!auth)
3080
- 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
+ }
3081
3144
  if (session.isActive()) return;
3082
3145
  await returningUserUnlock(auth.userId, auth.jwt);
3083
3146
  },
3084
3147
  isUnlocked() {
3085
3148
  return session.isActive();
3086
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
+ },
3087
3159
  sessionExpiresAt() {
3088
3160
  return session.expiresAtTimestamp();
3089
3161
  },
@@ -3176,10 +3248,12 @@ async function createDecaneConnect(config) {
3176
3248
  const tee = session.requireActive();
3177
3249
  const result = await tee.rotateShares(auth.jwt);
3178
3250
  try {
3251
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3252
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3179
3253
  const file = await buildRecoveryShareFile(
3180
3254
  result.recoveryShare,
3181
3255
  opts.password,
3182
- auth.addresses,
3256
+ addresses,
3183
3257
  result.shareSetVersion,
3184
3258
  opts.passwordHint
3185
3259
  );
@@ -3195,11 +3269,13 @@ async function createDecaneConnect(config) {
3195
3269
  const tee = session.requireActive();
3196
3270
  const result = await tee.exportPortableBackup(auth.jwt);
3197
3271
  try {
3272
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3273
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3198
3274
  return await buildPortableBackup(
3199
3275
  result.deviceShare,
3200
3276
  result.recoveryShare,
3201
3277
  opts.password,
3202
- auth.addresses,
3278
+ addresses,
3203
3279
  result.shareSetVersion,
3204
3280
  opts.passwordHint
3205
3281
  );
@@ -3271,6 +3347,17 @@ function b64uToBuffer(b64u) {
3271
3347
  function toHex(bytes) {
3272
3348
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3273
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
+ }
3274
3361
 
3275
3362
  // src/social/index.ts
3276
3363
  init_errors();
@@ -4196,6 +4283,15 @@ function countdown(expiresAt) {
4196
4283
  const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
4197
4284
  return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
4198
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
+ }
4199
4295
  function CopyButton({ value }) {
4200
4296
  const [copied, setCopied] = useState2(false);
4201
4297
  async function copy() {
@@ -4223,7 +4319,54 @@ function CopyButton({ value }) {
4223
4319
  copied ? "\u2713" : "copy"
4224
4320
  );
4225
4321
  }
4226
- 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);
4227
4370
  const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
4228
4371
  return /* @__PURE__ */ React3.createElement("div", { style: {
4229
4372
  background: "var(--dc-surface)",
@@ -4231,7 +4374,233 @@ function AddressRow({ label, address }) {
4231
4374
  borderRadius: 10,
4232
4375
  padding: "10px 14px",
4233
4376
  marginBottom: 8
4234
- } }, /* @__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.")));
4235
4604
  }
4236
4605
  function SocialWalletModal({
4237
4606
  open,
@@ -4239,7 +4608,15 @@ function SocialWalletModal({
4239
4608
  theme = "auto",
4240
4609
  socialAuth,
4241
4610
  onUnlock,
4242
- onDisconnect
4611
+ onDisconnect,
4612
+ onGoogleSignIn,
4613
+ onEmailStart,
4614
+ onEmailVerify,
4615
+ authMethods = ["google", "email"],
4616
+ onRotateShares,
4617
+ onExportPortableBackup,
4618
+ rpcUrls,
4619
+ chains
4243
4620
  }) {
4244
4621
  const [timer, setTimer] = useState2("");
4245
4622
  const [unlocking, setUnlocking] = useState2(false);
@@ -4288,6 +4665,9 @@ function SocialWalletModal({
4288
4665
  setDisconnecting(false);
4289
4666
  }
4290
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;
4291
4671
  const modal = /* @__PURE__ */ React3.createElement(
4292
4672
  "div",
4293
4673
  {
@@ -4325,10 +4705,14 @@ function SocialWalletModal({
4325
4705
  borderRadius: 20,
4326
4706
  width: "100%",
4327
4707
  maxWidth: 380,
4328
- boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
4329
- 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)"
4330
4711
  } }, /* @__PURE__ */ React3.createElement("div", { style: {
4331
4712
  height: 2,
4713
+ position: "sticky",
4714
+ top: 0,
4715
+ zIndex: 1,
4332
4716
  background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
4333
4717
  } }), /* @__PURE__ */ React3.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React3.createElement("div", { style: {
4334
4718
  width: 34,
@@ -4357,7 +4741,7 @@ function SocialWalletModal({
4357
4741
  strokeLinecap: "round",
4358
4742
  strokeLinejoin: "round"
4359
4743
  }
4360
- ))), /* @__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(
4361
4745
  "button",
4362
4746
  {
4363
4747
  onClick: onClose,
@@ -4377,7 +4761,16 @@ function SocialWalletModal({
4377
4761
  }
4378
4762
  },
4379
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" }))
4380
- )), /* @__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: {
4381
4774
  margin: "0 16px 14px",
4382
4775
  padding: "8px 14px",
4383
4776
  background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
@@ -4397,7 +4790,14 @@ function SocialWalletModal({
4397
4790
  fontSize: 12,
4398
4791
  fontWeight: 700,
4399
4792
  color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
4400
- } }, 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: {
4401
4801
  padding: "14px 16px",
4402
4802
  borderTop: "1px solid var(--dc-border)",
4403
4803
  display: "flex",
@@ -4442,7 +4842,7 @@ function SocialWalletModal({
4442
4842
  }
4443
4843
  },
4444
4844
  disconnecting ? "Signing out\u2026" : "Sign out"
4445
- )))
4845
+ ))))
4446
4846
  );
4447
4847
  return createPortal2(modal, document.body);
4448
4848
  }
@@ -4460,7 +4860,8 @@ var DEFAULT_SOCIAL = {
4460
4860
  addresses: null,
4461
4861
  isNewUser: false,
4462
4862
  isUnlocked: false,
4463
- sessionExpiresAt: null
4863
+ sessionExpiresAt: null,
4864
+ needsReconnect: false
4464
4865
  };
4465
4866
  var DecaneContext = createContext(null);
4466
4867
  function hasSocial(mode) {
@@ -4520,7 +4921,7 @@ function DecaneKit({ config, children }) {
4520
4921
  sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
4521
4922
  const saved = sdk.getAddresses();
4522
4923
  if (saved) {
4523
- setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
4924
+ setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4524
4925
  }
4525
4926
  const params = new URLSearchParams(window.location.search);
4526
4927
  if (params.has("decane_jwt") || params.has("decane_error")) {
@@ -4534,7 +4935,8 @@ function DecaneKit({ config, children }) {
4534
4935
  addresses: result.addresses,
4535
4936
  isNewUser: result.isNewUser,
4536
4937
  isUnlocked: sdk?.isUnlocked() ?? false,
4537
- sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
4938
+ sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4939
+ needsReconnect: false
4538
4940
  });
4539
4941
  }
4540
4942
  const connectWithGoogle = useCallback3(async () => {
@@ -4561,7 +4963,8 @@ function DecaneKit({ config, children }) {
4561
4963
  setSocialAuth((p) => ({
4562
4964
  ...p,
4563
4965
  isUnlocked: sdk.isUnlocked(),
4564
- sessionExpiresAt: sdk.sessionExpiresAt()
4966
+ sessionExpiresAt: sdk.sessionExpiresAt(),
4967
+ needsReconnect: false
4565
4968
  }));
4566
4969
  }, []);
4567
4970
  const socialDisconnect = useCallback3(async () => {
@@ -4585,6 +4988,31 @@ function DecaneKit({ config, children }) {
4585
4988
  if (!sdk) throw new Error("Social wallet not connected");
4586
4989
  return sdk.signSolanaTransaction(tx);
4587
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
+ }, []);
4588
5016
  const handleWalletConnect = useCallback3(
4589
5017
  (wallet, preference) => {
4590
5018
  walletRef.current.connect(wallet, preference).catch(console.error);
@@ -4612,6 +5040,11 @@ function DecaneKit({ config, children }) {
4612
5040
  socialSignMessage,
4613
5041
  socialSendTransaction,
4614
5042
  socialSignSolanaTransaction,
5043
+ socialRotateShares,
5044
+ socialExportPortableBackup,
5045
+ socialHasRecoveryShare,
5046
+ socialShouldPromptRecovery,
5047
+ socialDismissRecoveryPrompt,
4615
5048
  socialModalOpen,
4616
5049
  openSocialModal,
4617
5050
  closeSocialModal
@@ -4642,7 +5075,15 @@ function DecaneKit({ config, children }) {
4642
5075
  theme,
4643
5076
  socialAuth,
4644
5077
  onUnlock: socialUnlock,
4645
- 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
4646
5087
  }
4647
5088
  )
4648
5089
  );
@@ -4730,6 +5171,11 @@ function useSocialAuth() {
4730
5171
  isNewUser: socialAuth.isNewUser,
4731
5172
  addresses: socialAuth.addresses,
4732
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,
4733
5179
  googleLoading,
4734
5180
  emailLoading,
4735
5181
  error,
@@ -4749,6 +5195,11 @@ function useSocialWallet() {
4749
5195
  socialSignMessage,
4750
5196
  socialSendTransaction,
4751
5197
  socialSignSolanaTransaction,
5198
+ socialRotateShares,
5199
+ socialExportPortableBackup,
5200
+ socialHasRecoveryShare,
5201
+ socialShouldPromptRecovery,
5202
+ socialDismissRecoveryPrompt,
4752
5203
  openSocialModal,
4753
5204
  closeSocialModal,
4754
5205
  socialModalOpen
@@ -4798,6 +5249,26 @@ function useSocialWallet() {
4798
5249
  throw e;
4799
5250
  }
4800
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
+ );
4801
5272
  return {
4802
5273
  // State
4803
5274
  isConnected: !!socialAuth.addresses,
@@ -4805,6 +5276,11 @@ function useSocialWallet() {
4805
5276
  isNewUser: socialAuth.isNewUser,
4806
5277
  addresses: socialAuth.addresses,
4807
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,
4808
5284
  // Modal
4809
5285
  modalOpen: socialModalOpen,
4810
5286
  openModal: openSocialModal,
@@ -4816,6 +5292,12 @@ function useSocialWallet() {
4816
5292
  signMessage,
4817
5293
  sendTransaction,
4818
5294
  signSolanaTransaction: signSolanaTransaction2,
5295
+ // Recovery (2-of-3 Shamir share set)
5296
+ rotateShares,
5297
+ exportPortableBackup,
5298
+ hasRecoveryShare,
5299
+ shouldPromptRecovery,
5300
+ dismissRecoveryPrompt,
4819
5301
  loading,
4820
5302
  error,
4821
5303
  clearError: () => setError(null)
@@ -5061,6 +5543,7 @@ export {
5061
5543
  NewDeviceError,
5062
5544
  OriginNotAllowedError,
5063
5545
  PromptPinNotConfiguredError,
5546
+ ReconnectRequiredError,
5064
5547
  RecoveryFailedError,
5065
5548
  RecoveryWalletMismatchError,
5066
5549
  SessionExpiredError,