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.cjs CHANGED
@@ -44,6 +44,7 @@ __export(errors_exports, {
44
44
  NewDeviceError: () => NewDeviceError,
45
45
  OriginNotAllowedError: () => OriginNotAllowedError,
46
46
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
47
+ ReconnectRequiredError: () => ReconnectRequiredError,
47
48
  RecoveryFailedError: () => RecoveryFailedError,
48
49
  RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
49
50
  SessionExpiredError: () => SessionExpiredError,
@@ -53,7 +54,7 @@ __export(errors_exports, {
53
54
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
54
55
  WrongPasswordError: () => WrongPasswordError
55
56
  });
56
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
57
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
57
58
  var init_errors = __esm({
58
59
  "src/social/errors.ts"() {
59
60
  "use strict";
@@ -85,6 +86,15 @@ var init_errors = __esm({
85
86
  super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
86
87
  }
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
+ };
88
98
  UserCancelledError = class extends DecaneConnectError {
89
99
  constructor() {
90
100
  super("USER_CANCELLED", "User cancelled the operation", true);
@@ -181,6 +191,7 @@ __export(index_exports, {
181
191
  NewDeviceError: () => NewDeviceError,
182
192
  OriginNotAllowedError: () => OriginNotAllowedError,
183
193
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
194
+ ReconnectRequiredError: () => ReconnectRequiredError,
184
195
  RecoveryFailedError: () => RecoveryFailedError,
185
196
  RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
186
197
  SessionExpiredError: () => SessionExpiredError,
@@ -2228,6 +2239,13 @@ var TeeClient = class {
2228
2239
  await verifyTeeAttestation(this.teeUrl, nonceHex);
2229
2240
  this.attested = true;
2230
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
+ */
2231
2249
  async openSession(deviceShare, jwt) {
2232
2250
  await this.ensureAttested();
2233
2251
  const res = await fetch(`${this.teeUrl}/signer/session`, {
@@ -2236,7 +2254,9 @@ var TeeClient = class {
2236
2254
  body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2237
2255
  });
2238
2256
  if (!res.ok) throw new Error("session open failed");
2239
- this.handle = (await res.json()).handle;
2257
+ const body = await res.json();
2258
+ this.handle = body.handle;
2259
+ return body.addresses ?? null;
2240
2260
  }
2241
2261
  /**
2242
2262
  * Flow B: exchange a recovery share (from a recovery-share file) for a
@@ -2369,9 +2389,24 @@ var SessionManager = class {
2369
2389
  window.addEventListener("pagehide", () => this.end());
2370
2390
  }
2371
2391
  }
2372
- 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) {
2373
2401
  this.tee = new TeeClient(this.teeUrl);
2374
- 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
+ }
2375
2410
  this.evmAddress = addresses.evm;
2376
2411
  this.solanaAddress = addresses.solana;
2377
2412
  this.expiresAt = Date.now() + this.durationMs;
@@ -2381,6 +2416,7 @@ var SessionManager = class {
2381
2416
  this.events.emit("session-expired");
2382
2417
  }, this.durationMs);
2383
2418
  this.events.emit("session-started");
2419
+ return addresses;
2384
2420
  }
2385
2421
  end() {
2386
2422
  void this.tee?.closeSession();
@@ -2431,6 +2467,11 @@ async function splitMaster(master, version = 1) {
2431
2467
  if (parts.length !== 3) throw new Error("unexpected share count");
2432
2468
  return { device: parts[0], server: parts[1], recovery: parts[2], version };
2433
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
+ }
2434
2475
 
2435
2476
  // src/social/crypto/wrap.ts
2436
2477
  function buf(u) {
@@ -2875,6 +2916,18 @@ async function createDecaneConnect(config) {
2875
2916
  }
2876
2917
  };
2877
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
+ }
2878
2931
  async function getWrappingKey(stored) {
2879
2932
  if (stored.unlockMethod === "passkey") {
2880
2933
  const prfOutput = await getPrfOutput({
@@ -3010,8 +3063,15 @@ async function createDecaneConnect(config) {
3010
3063
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
3011
3064
  }
3012
3065
  }
3013
- await session.start(shareSet.device, jwt, addresses);
3066
+ const confirmed = await session.start(shareSet.device, jwt, addresses);
3014
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;
3015
3075
  if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
3016
3076
  try {
3017
3077
  const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
@@ -3035,15 +3095,15 @@ async function createDecaneConnect(config) {
3035
3095
  }
3036
3096
  async function returningUserUnlock(userId, jwt) {
3037
3097
  const rawAddresses = await api.getAddresses(jwt);
3038
- if (!rawAddresses.evm || !rawAddresses.solana) {
3039
- throw new CallSequenceError(
3040
- "No addresses are recorded for this user \u2014 inconsistent server state"
3041
- );
3042
- }
3043
- const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
3098
+ const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
3044
3099
  const stored = await storage.getShare(userId);
3045
3100
  let deviceShare;
3046
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
+ }
3047
3107
  deviceShare = await recoverDeviceShare({
3048
3108
  userId,
3049
3109
  jwt,
@@ -3054,13 +3114,11 @@ async function createDecaneConnect(config) {
3054
3114
  onProgress: config.onRecoveryProgress,
3055
3115
  promptForRecoveryFile: config.promptForRecoveryFile,
3056
3116
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
3057
- if (!config.onRecoveryRotated) {
3058
- 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.");
3059
- return;
3060
- }
3061
- const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: addresses.evm, solanaAddress: addresses.solana });
3062
- const file = await buildRecoveryShareFile(recoveryShare, password, addresses, shareSetVersion, passwordHint);
3063
- await config.onRecoveryFileReady?.(file, recoveryShareFileName(addresses.evm));
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));
3064
3122
  await api.markRecoveryShareSaved(jwt);
3065
3123
  }
3066
3124
  });
@@ -3079,8 +3137,13 @@ async function createDecaneConnect(config) {
3079
3137
  throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
3080
3138
  }
3081
3139
  }
3082
- await session.start(deviceShare, jwt, addresses);
3140
+ const addresses = await session.start(deviceShare, jwt, recordedAddresses);
3083
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
+ }
3084
3147
  return addresses;
3085
3148
  }
3086
3149
  async function handleAuthResult(result) {
@@ -3142,14 +3205,24 @@ async function createDecaneConnect(config) {
3142
3205
  return handleAuthResult(result);
3143
3206
  },
3144
3207
  async unlock() {
3145
- if (!auth)
3146
- 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
+ }
3147
3211
  if (session.isActive()) return;
3148
3212
  await returningUserUnlock(auth.userId, auth.jwt);
3149
3213
  },
3150
3214
  isUnlocked() {
3151
3215
  return session.isActive();
3152
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
+ },
3153
3226
  sessionExpiresAt() {
3154
3227
  return session.expiresAtTimestamp();
3155
3228
  },
@@ -3242,10 +3315,12 @@ async function createDecaneConnect(config) {
3242
3315
  const tee = session.requireActive();
3243
3316
  const result = await tee.rotateShares(auth.jwt);
3244
3317
  try {
3318
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3319
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3245
3320
  const file = await buildRecoveryShareFile(
3246
3321
  result.recoveryShare,
3247
3322
  opts.password,
3248
- auth.addresses,
3323
+ addresses,
3249
3324
  result.shareSetVersion,
3250
3325
  opts.passwordHint
3251
3326
  );
@@ -3261,11 +3336,13 @@ async function createDecaneConnect(config) {
3261
3336
  const tee = session.requireActive();
3262
3337
  const result = await tee.exportPortableBackup(auth.jwt);
3263
3338
  try {
3339
+ await persistRotatedDeviceShare(auth.userId, result.deviceShare);
3340
+ const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
3264
3341
  return await buildPortableBackup(
3265
3342
  result.deviceShare,
3266
3343
  result.recoveryShare,
3267
3344
  opts.password,
3268
- auth.addresses,
3345
+ addresses,
3269
3346
  result.shareSetVersion,
3270
3347
  opts.passwordHint
3271
3348
  );
@@ -3337,6 +3414,17 @@ function b64uToBuffer(b64u) {
3337
3414
  function toHex(bytes) {
3338
3415
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3339
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
+ }
3340
3428
 
3341
3429
  // src/social/index.ts
3342
3430
  init_errors();
@@ -4262,6 +4350,15 @@ function countdown(expiresAt) {
4262
4350
  const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
4263
4351
  return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
4264
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
+ }
4265
4362
  function CopyButton({ value }) {
4266
4363
  const [copied, setCopied] = (0, import_react3.useState)(false);
4267
4364
  async function copy() {
@@ -4289,7 +4386,54 @@ function CopyButton({ value }) {
4289
4386
  copied ? "\u2713" : "copy"
4290
4387
  );
4291
4388
  }
4292
- 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);
4293
4437
  const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
4294
4438
  return /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4295
4439
  background: "var(--dc-surface)",
@@ -4297,7 +4441,233 @@ function AddressRow({ label, address }) {
4297
4441
  borderRadius: 10,
4298
4442
  padding: "10px 14px",
4299
4443
  marginBottom: 8
4300
- } }, /* @__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.")));
4301
4671
  }
4302
4672
  function SocialWalletModal({
4303
4673
  open,
@@ -4305,7 +4675,15 @@ function SocialWalletModal({
4305
4675
  theme = "auto",
4306
4676
  socialAuth,
4307
4677
  onUnlock,
4308
- onDisconnect
4678
+ onDisconnect,
4679
+ onGoogleSignIn,
4680
+ onEmailStart,
4681
+ onEmailVerify,
4682
+ authMethods = ["google", "email"],
4683
+ onRotateShares,
4684
+ onExportPortableBackup,
4685
+ rpcUrls,
4686
+ chains
4309
4687
  }) {
4310
4688
  const [timer, setTimer] = (0, import_react3.useState)("");
4311
4689
  const [unlocking, setUnlocking] = (0, import_react3.useState)(false);
@@ -4354,6 +4732,9 @@ function SocialWalletModal({
4354
4732
  setDisconnecting(false);
4355
4733
  }
4356
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;
4357
4738
  const modal = /* @__PURE__ */ import_react3.default.createElement(
4358
4739
  "div",
4359
4740
  {
@@ -4391,10 +4772,14 @@ function SocialWalletModal({
4391
4772
  borderRadius: 20,
4392
4773
  width: "100%",
4393
4774
  maxWidth: 380,
4394
- boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)",
4395
- 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)"
4396
4778
  } }, /* @__PURE__ */ import_react3.default.createElement("div", { style: {
4397
4779
  height: 2,
4780
+ position: "sticky",
4781
+ top: 0,
4782
+ zIndex: 1,
4398
4783
  background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
4399
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: {
4400
4785
  width: 34,
@@ -4423,7 +4808,7 @@ function SocialWalletModal({
4423
4808
  strokeLinecap: "round",
4424
4809
  strokeLinejoin: "round"
4425
4810
  }
4426
- ))), /* @__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(
4427
4812
  "button",
4428
4813
  {
4429
4814
  onClick: onClose,
@@ -4443,7 +4828,16 @@ function SocialWalletModal({
4443
4828
  }
4444
4829
  },
4445
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" }))
4446
- )), /* @__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: {
4447
4841
  margin: "0 16px 14px",
4448
4842
  padding: "8px 14px",
4449
4843
  background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
@@ -4463,7 +4857,14 @@ function SocialWalletModal({
4463
4857
  fontSize: 12,
4464
4858
  fontWeight: 700,
4465
4859
  color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
4466
- } }, 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: {
4467
4868
  padding: "14px 16px",
4468
4869
  borderTop: "1px solid var(--dc-border)",
4469
4870
  display: "flex",
@@ -4508,7 +4909,7 @@ function SocialWalletModal({
4508
4909
  }
4509
4910
  },
4510
4911
  disconnecting ? "Signing out\u2026" : "Sign out"
4511
- )))
4912
+ ))))
4512
4913
  );
4513
4914
  return (0, import_react_dom2.createPortal)(modal, document.body);
4514
4915
  }
@@ -4519,7 +4920,8 @@ var DEFAULT_SOCIAL = {
4519
4920
  addresses: null,
4520
4921
  isNewUser: false,
4521
4922
  isUnlocked: false,
4522
- sessionExpiresAt: null
4923
+ sessionExpiresAt: null,
4924
+ needsReconnect: false
4523
4925
  };
4524
4926
  var DecaneContext = (0, import_react4.createContext)(null);
4525
4927
  function hasSocial(mode) {
@@ -4579,7 +4981,7 @@ function DecaneKit({ config, children }) {
4579
4981
  sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
4580
4982
  const saved = sdk.getAddresses();
4581
4983
  if (saved) {
4582
- setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
4984
+ setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
4583
4985
  }
4584
4986
  const params = new URLSearchParams(window.location.search);
4585
4987
  if (params.has("decane_jwt") || params.has("decane_error")) {
@@ -4593,7 +4995,8 @@ function DecaneKit({ config, children }) {
4593
4995
  addresses: result.addresses,
4594
4996
  isNewUser: result.isNewUser,
4595
4997
  isUnlocked: sdk?.isUnlocked() ?? false,
4596
- sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
4998
+ sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
4999
+ needsReconnect: false
4597
5000
  });
4598
5001
  }
4599
5002
  const connectWithGoogle = (0, import_react4.useCallback)(async () => {
@@ -4620,7 +5023,8 @@ function DecaneKit({ config, children }) {
4620
5023
  setSocialAuth((p) => ({
4621
5024
  ...p,
4622
5025
  isUnlocked: sdk.isUnlocked(),
4623
- sessionExpiresAt: sdk.sessionExpiresAt()
5026
+ sessionExpiresAt: sdk.sessionExpiresAt(),
5027
+ needsReconnect: false
4624
5028
  }));
4625
5029
  }, []);
4626
5030
  const socialDisconnect = (0, import_react4.useCallback)(async () => {
@@ -4644,6 +5048,31 @@ function DecaneKit({ config, children }) {
4644
5048
  if (!sdk) throw new Error("Social wallet not connected");
4645
5049
  return sdk.signSolanaTransaction(tx);
4646
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
+ }, []);
4647
5076
  const handleWalletConnect = (0, import_react4.useCallback)(
4648
5077
  (wallet, preference) => {
4649
5078
  walletRef.current.connect(wallet, preference).catch(console.error);
@@ -4671,6 +5100,11 @@ function DecaneKit({ config, children }) {
4671
5100
  socialSignMessage,
4672
5101
  socialSendTransaction,
4673
5102
  socialSignSolanaTransaction,
5103
+ socialRotateShares,
5104
+ socialExportPortableBackup,
5105
+ socialHasRecoveryShare,
5106
+ socialShouldPromptRecovery,
5107
+ socialDismissRecoveryPrompt,
4674
5108
  socialModalOpen,
4675
5109
  openSocialModal,
4676
5110
  closeSocialModal
@@ -4701,7 +5135,15 @@ function DecaneKit({ config, children }) {
4701
5135
  theme,
4702
5136
  socialAuth,
4703
5137
  onUnlock: socialUnlock,
4704
- 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
4705
5147
  }
4706
5148
  )
4707
5149
  );
@@ -4789,6 +5231,11 @@ function useSocialAuth() {
4789
5231
  isNewUser: socialAuth.isNewUser,
4790
5232
  addresses: socialAuth.addresses,
4791
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,
4792
5239
  googleLoading,
4793
5240
  emailLoading,
4794
5241
  error,
@@ -4808,6 +5255,11 @@ function useSocialWallet() {
4808
5255
  socialSignMessage,
4809
5256
  socialSendTransaction,
4810
5257
  socialSignSolanaTransaction,
5258
+ socialRotateShares,
5259
+ socialExportPortableBackup,
5260
+ socialHasRecoveryShare,
5261
+ socialShouldPromptRecovery,
5262
+ socialDismissRecoveryPrompt,
4811
5263
  openSocialModal,
4812
5264
  closeSocialModal,
4813
5265
  socialModalOpen
@@ -4857,6 +5309,26 @@ function useSocialWallet() {
4857
5309
  throw e;
4858
5310
  }
4859
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
+ );
4860
5332
  return {
4861
5333
  // State
4862
5334
  isConnected: !!socialAuth.addresses,
@@ -4864,6 +5336,11 @@ function useSocialWallet() {
4864
5336
  isNewUser: socialAuth.isNewUser,
4865
5337
  addresses: socialAuth.addresses,
4866
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,
4867
5344
  // Modal
4868
5345
  modalOpen: socialModalOpen,
4869
5346
  openModal: openSocialModal,
@@ -4875,6 +5352,12 @@ function useSocialWallet() {
4875
5352
  signMessage,
4876
5353
  sendTransaction,
4877
5354
  signSolanaTransaction: signSolanaTransaction2,
5355
+ // Recovery (2-of-3 Shamir share set)
5356
+ rotateShares,
5357
+ exportPortableBackup,
5358
+ hasRecoveryShare,
5359
+ shouldPromptRecovery,
5360
+ dismissRecoveryPrompt,
4878
5361
  loading,
4879
5362
  error,
4880
5363
  clearError: () => setError(null)
@@ -5121,6 +5604,7 @@ function useSignTypedData() {
5121
5604
  NewDeviceError,
5122
5605
  OriginNotAllowedError,
5123
5606
  PromptPinNotConfiguredError,
5607
+ ReconnectRequiredError,
5124
5608
  RecoveryFailedError,
5125
5609
  RecoveryWalletMismatchError,
5126
5610
  SessionExpiredError,