decane-connect-kit 1.0.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,145 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/social/errors.ts
12
+ var errors_exports = {};
13
+ __export(errors_exports, {
14
+ AttestationError: () => AttestationError,
15
+ CallSequenceError: () => CallSequenceError,
16
+ DecaneConnectError: () => DecaneConnectError,
17
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
18
+ InvalidApiKeyError: () => InvalidApiKeyError,
19
+ InvalidPinError: () => InvalidPinError,
20
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
21
+ NetworkError: () => NetworkError,
22
+ NewDeviceError: () => NewDeviceError,
23
+ OriginNotAllowedError: () => OriginNotAllowedError,
24
+ PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
25
+ RecoveryFailedError: () => RecoveryFailedError,
26
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
27
+ SessionExpiredError: () => SessionExpiredError,
28
+ ShareCorruptedError: () => ShareCorruptedError,
29
+ UnlockRequiredError: () => UnlockRequiredError,
30
+ UserCancelledError: () => UserCancelledError,
31
+ WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
32
+ WrongPasswordError: () => WrongPasswordError
33
+ });
34
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
35
+ var init_errors = __esm({
36
+ "src/social/errors.ts"() {
37
+ "use strict";
38
+ DecaneConnectError = class extends Error {
39
+ constructor(code, message, retryable = false) {
40
+ super(message);
41
+ this.name = "DecaneConnectError";
42
+ this.code = code;
43
+ this.retryable = retryable;
44
+ }
45
+ };
46
+ InvalidApiKeyError = class extends DecaneConnectError {
47
+ constructor() {
48
+ super("INVALID_API_KEY", "API key is invalid or revoked");
49
+ }
50
+ };
51
+ OriginNotAllowedError = class extends DecaneConnectError {
52
+ constructor(origin) {
53
+ super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
54
+ }
55
+ };
56
+ SessionExpiredError = class extends DecaneConnectError {
57
+ constructor() {
58
+ super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
59
+ }
60
+ };
61
+ UnlockRequiredError = class extends DecaneConnectError {
62
+ constructor() {
63
+ super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
64
+ }
65
+ };
66
+ UserCancelledError = class extends DecaneConnectError {
67
+ constructor() {
68
+ super("USER_CANCELLED", "User cancelled the operation", true);
69
+ }
70
+ };
71
+ WebAuthnNotSupportedError = class extends DecaneConnectError {
72
+ constructor() {
73
+ super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
74
+ }
75
+ };
76
+ InvalidPinError = class extends DecaneConnectError {
77
+ constructor() {
78
+ super("INVALID_PIN", "Incorrect PIN", true);
79
+ }
80
+ };
81
+ CallSequenceError = class extends DecaneConnectError {
82
+ constructor(msg) {
83
+ super("CALL_SEQUENCE", msg);
84
+ }
85
+ };
86
+ NetworkError = class extends DecaneConnectError {
87
+ constructor() {
88
+ super("NETWORK", "Network request failed", true);
89
+ }
90
+ };
91
+ ShareCorruptedError = class extends DecaneConnectError {
92
+ constructor() {
93
+ super("SHARE_CORRUPTED", "Stored device share is corrupted");
94
+ }
95
+ };
96
+ NewDeviceError = class extends DecaneConnectError {
97
+ constructor() {
98
+ super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
99
+ }
100
+ };
101
+ PromptPinNotConfiguredError = class extends DecaneConnectError {
102
+ constructor() {
103
+ super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
104
+ }
105
+ };
106
+ AttestationError = class extends DecaneConnectError {
107
+ constructor(msg) {
108
+ super("ATTESTATION_FAILED", msg);
109
+ }
110
+ };
111
+ RecoveryFailedError = class extends DecaneConnectError {
112
+ constructor(reason) {
113
+ super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
114
+ }
115
+ };
116
+ InvalidRecoveryFileError = class extends DecaneConnectError {
117
+ constructor() {
118
+ super("INVALID_RECOVERY_FILE", "This does not look like a valid recovery file", false);
119
+ }
120
+ };
121
+ WrongPasswordError = class extends DecaneConnectError {
122
+ constructor() {
123
+ super("WRONG_PASSWORD", "Incorrect password for this recovery file", true);
124
+ }
125
+ };
126
+ RecoveryWalletMismatchError = class extends DecaneConnectError {
127
+ constructor() {
128
+ super(
129
+ "RECOVERY_WALLET_MISMATCH",
130
+ "This recovery file is for a different wallet than the account you are signed into",
131
+ false
132
+ );
133
+ }
134
+ };
135
+ ExportNotAuthorizedError = class extends DecaneConnectError {
136
+ constructor() {
137
+ super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
138
+ }
139
+ };
140
+ }
141
+ });
142
+
1
143
  // src/core/emitter.ts
2
144
  function createEmitter() {
3
145
  const listeners = /* @__PURE__ */ new Map();
@@ -1734,94 +1876,10 @@ function createDecane(config = {}) {
1734
1876
  }
1735
1877
 
1736
1878
  // src/social/client.ts
1879
+ init_errors();
1737
1880
  import { Wallet } from "ethers";
1738
1881
  import { Transaction as Transaction2, VersionedTransaction, Keypair } from "@solana/web3.js";
1739
1882
 
1740
- // src/social/errors.ts
1741
- var DecaneConnectError = class extends Error {
1742
- constructor(code, message, retryable = false) {
1743
- super(message);
1744
- this.name = "DecaneConnectError";
1745
- this.code = code;
1746
- this.retryable = retryable;
1747
- }
1748
- };
1749
- var InvalidApiKeyError = class extends DecaneConnectError {
1750
- constructor() {
1751
- super("INVALID_API_KEY", "API key is invalid or revoked");
1752
- }
1753
- };
1754
- var OriginNotAllowedError = class extends DecaneConnectError {
1755
- constructor(origin) {
1756
- super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
1757
- }
1758
- };
1759
- var SessionExpiredError = class extends DecaneConnectError {
1760
- constructor() {
1761
- super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
1762
- }
1763
- };
1764
- var UnlockRequiredError = class extends DecaneConnectError {
1765
- constructor() {
1766
- super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
1767
- }
1768
- };
1769
- var UserCancelledError = class extends DecaneConnectError {
1770
- constructor() {
1771
- super("USER_CANCELLED", "User cancelled the operation", true);
1772
- }
1773
- };
1774
- var WebAuthnNotSupportedError = class extends DecaneConnectError {
1775
- constructor() {
1776
- super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
1777
- }
1778
- };
1779
- var InvalidPinError = class extends DecaneConnectError {
1780
- constructor() {
1781
- super("INVALID_PIN", "Incorrect PIN", true);
1782
- }
1783
- };
1784
- var CallSequenceError = class extends DecaneConnectError {
1785
- constructor(msg) {
1786
- super("CALL_SEQUENCE", msg);
1787
- }
1788
- };
1789
- var NetworkError = class extends DecaneConnectError {
1790
- constructor() {
1791
- super("NETWORK", "Network request failed", true);
1792
- }
1793
- };
1794
- var ShareCorruptedError = class extends DecaneConnectError {
1795
- constructor() {
1796
- super("SHARE_CORRUPTED", "Stored device share is corrupted");
1797
- }
1798
- };
1799
- var NewDeviceError = class extends DecaneConnectError {
1800
- constructor() {
1801
- super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
1802
- }
1803
- };
1804
- var PromptPinNotConfiguredError = class extends DecaneConnectError {
1805
- constructor() {
1806
- super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
1807
- }
1808
- };
1809
- var AttestationError = class extends DecaneConnectError {
1810
- constructor(msg) {
1811
- super("ATTESTATION_FAILED", msg);
1812
- }
1813
- };
1814
- var RecoveryFailedError = class extends DecaneConnectError {
1815
- constructor(reason) {
1816
- super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
1817
- }
1818
- };
1819
- var InvalidPhraseError = class extends DecaneConnectError {
1820
- constructor() {
1821
- super("INVALID_PHRASE", "Recovery phrase is invalid or corrupted", true);
1822
- }
1823
- };
1824
-
1825
1883
  // src/social/events.ts
1826
1884
  var EventEmitter = class {
1827
1885
  constructor() {
@@ -1840,6 +1898,7 @@ var EventEmitter = class {
1840
1898
  };
1841
1899
 
1842
1900
  // src/social/api.ts
1901
+ init_errors();
1843
1902
  var ERROR_MAP = {
1844
1903
  INVALID_API_KEY: InvalidApiKeyError,
1845
1904
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
@@ -1910,13 +1969,26 @@ var ApiClient = class {
1910
1969
  })
1911
1970
  });
1912
1971
  }
1972
+ // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
1973
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
1974
+ return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
1975
+ headers: { Authorization: `Bearer ${jwt}` }
1976
+ });
1977
+ }
1978
+ async postWebAuthnRegister(jwt, response, origin) {
1979
+ await this._request("/webauthn/register", {
1980
+ method: "POST",
1981
+ headers: { Authorization: `Bearer ${jwt}` },
1982
+ body: JSON.stringify({ response, origin })
1983
+ });
1984
+ }
1913
1985
  async revoke(jwt) {
1914
1986
  await this._request("/auth/revoke", {
1915
1987
  method: "POST",
1916
1988
  headers: { Authorization: `Bearer ${jwt}` }
1917
1989
  });
1918
1990
  }
1919
- // ── Recovery (passkey-sync backup + phrase-saved flag) ────────────────────
1991
+ // ── Recovery (passkey-sync backup + recovery-share-saved flag) ────────────
1920
1992
  async hasRecoveryBackup(jwt) {
1921
1993
  const data = await this._request("/recovery/backup/exists", {
1922
1994
  headers: { Authorization: `Bearer ${jwt}` }
@@ -1957,8 +2029,19 @@ var ApiClient = class {
1957
2029
  headers: { Authorization: `Bearer ${jwt}` }
1958
2030
  });
1959
2031
  }
1960
- async markPhraseSaved(jwt) {
1961
- await this._request("/recovery/phrase-saved", {
2032
+ async markRecoveryShareSaved(jwt) {
2033
+ await this._request("/recovery/share-saved", {
2034
+ method: "POST",
2035
+ headers: { Authorization: `Bearer ${jwt}` }
2036
+ });
2037
+ }
2038
+ async getRecoveryStatus(jwt) {
2039
+ return this._request("/recovery/status", {
2040
+ headers: { Authorization: `Bearer ${jwt}` }
2041
+ });
2042
+ }
2043
+ async dismissRecoveryNag(jwt) {
2044
+ await this._request("/recovery/nag-dismissed", {
1962
2045
  method: "POST",
1963
2046
  headers: { Authorization: `Bearer ${jwt}` }
1964
2047
  });
@@ -1967,8 +2050,8 @@ var ApiClient = class {
1967
2050
  function bytesToB64(bytes) {
1968
2051
  return btoa(String.fromCharCode(...bytes));
1969
2052
  }
1970
- function b64ToBytes(b642) {
1971
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
2053
+ function b64ToBytes(b644) {
2054
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
1972
2055
  }
1973
2056
 
1974
2057
  // src/social/storage.ts
@@ -1994,7 +2077,11 @@ var Storage = class {
1994
2077
  }
1995
2078
  };
1996
2079
 
2080
+ // src/social/teeClient.ts
2081
+ import { startAuthentication } from "@simplewebauthn/browser";
2082
+
1997
2083
  // src/social/attestation.ts
2084
+ init_errors();
1998
2085
  import { getCollateralAndVerify } from "@phala/dcap-qvl";
1999
2086
 
2000
2087
  // ../../infra/tee-measurement.json
@@ -2053,11 +2140,15 @@ async function verifyTeeAttestation(teeUrl, nonce) {
2053
2140
  }
2054
2141
 
2055
2142
  // src/social/teeClient.ts
2143
+ init_errors();
2056
2144
  function bytesToBase64(b) {
2057
2145
  let s = "";
2058
2146
  for (const byte of b) s += String.fromCharCode(byte);
2059
2147
  return btoa(s);
2060
2148
  }
2149
+ function base64ToBytes(s) {
2150
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2151
+ }
2061
2152
  var TeeClient = class {
2062
2153
  constructor(teeUrl) {
2063
2154
  this.teeUrl = teeUrl;
@@ -2081,16 +2172,90 @@ var TeeClient = class {
2081
2172
  if (!res.ok) throw new Error("session open failed");
2082
2173
  this.handle = (await res.json()).handle;
2083
2174
  }
2084
- /** Verify a candidate device share (from a recovery phrase) without exposing shares. */
2085
- async verifyRecoveryCandidate(deviceShare, jwt) {
2175
+ /**
2176
+ * Flow B: exchange a recovery share (from a recovery-share file) for a
2177
+ * fresh device share. The server verifies the reconstructed master
2178
+ * actually matches this account's addresses before returning anything —
2179
+ * see the security note in the TEE's /signer/recover handler. Rotates the
2180
+ * whole share set: the caller MUST persist the returned recoveryShare as a
2181
+ * new recovery-share file, since the old one stops working immediately.
2182
+ */
2183
+ async recoverWithShare(jwt, recoveryShare) {
2086
2184
  await this.ensureAttested();
2087
- const res = await fetch(`${this.teeUrl}/signer/verify-recovery`, {
2185
+ const res = await fetch(`${this.teeUrl}/signer/recover`, {
2088
2186
  method: "POST",
2089
2187
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2090
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2188
+ body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
2091
2189
  });
2092
- if (!res.ok) return false;
2093
- return (await res.json()).verified === true;
2190
+ if (res.status === 403) {
2191
+ const { RecoveryWalletMismatchError: RecoveryWalletMismatchError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2192
+ throw new RecoveryWalletMismatchError2();
2193
+ }
2194
+ if (!res.ok) throw new Error("recovery failed");
2195
+ const body = await res.json();
2196
+ return {
2197
+ deviceShare: base64ToBytes(body.deviceShare),
2198
+ recoveryShare: base64ToBytes(body.recoveryShare),
2199
+ shareSetVersion: body.shareSetVersion
2200
+ };
2201
+ }
2202
+ /**
2203
+ * Request a fresh WebAuthn assertion challenge and complete it in the
2204
+ * browser via @simplewebauthn/browser's startAuthentication(), which
2205
+ * handles all the base64url (de)serialization the raw navigator.credentials
2206
+ * API requires. Shared by rotateShares() and exportPortableBackup() — both
2207
+ * require proof of fresh authenticator possession, not just an active session.
2208
+ */
2209
+ async completeAssertion(jwt) {
2210
+ const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2211
+ method: "POST",
2212
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2213
+ body: JSON.stringify({ rpId: window.location.hostname })
2214
+ });
2215
+ if (!res.ok) throw new ExportNotAuthorizedError();
2216
+ const { challengeId, options } = await res.json();
2217
+ let assertion;
2218
+ try {
2219
+ assertion = await startAuthentication({ optionsJSON: options });
2220
+ } catch {
2221
+ const { UserCancelledError: UserCancelledError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2222
+ throw new UserCancelledError2();
2223
+ }
2224
+ return { challengeId, assertion };
2225
+ }
2226
+ /** Voluntary rotation — e.g. after a suspected device compromise, or to invalidate an old recovery file. */
2227
+ async rotateShares(jwt) {
2228
+ if (!this.handle) throw new Error("no session");
2229
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2230
+ const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2231
+ method: "POST",
2232
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2233
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2234
+ });
2235
+ if (!res.ok) throw new ExportNotAuthorizedError();
2236
+ const body = await res.json();
2237
+ return {
2238
+ deviceShare: base64ToBytes(body.deviceShare),
2239
+ recoveryShare: base64ToBytes(body.recoveryShare),
2240
+ shareSetVersion: body.shareSetVersion
2241
+ };
2242
+ }
2243
+ /** The two-share bearer portable backup — see recovery/portableBackup.ts. Rotates the set on export. */
2244
+ async exportPortableBackup(jwt) {
2245
+ if (!this.handle) throw new Error("no session");
2246
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2247
+ const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2248
+ method: "POST",
2249
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2250
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2251
+ });
2252
+ if (!res.ok) throw new ExportNotAuthorizedError();
2253
+ const body = await res.json();
2254
+ return {
2255
+ deviceShare: base64ToBytes(body.deviceShare),
2256
+ recoveryShare: base64ToBytes(body.recoveryShare),
2257
+ shareSetVersion: body.shareSetVersion
2258
+ };
2094
2259
  }
2095
2260
  hasSession() {
2096
2261
  return this.handle !== null;
@@ -2123,6 +2288,7 @@ var TeeClient = class {
2123
2288
  };
2124
2289
 
2125
2290
  // src/social/session.ts
2291
+ init_errors();
2126
2292
  var SessionManager = class {
2127
2293
  constructor(events, durationMs, teeUrl) {
2128
2294
  this.events = events;
@@ -2178,24 +2344,27 @@ var SessionManager = class {
2178
2344
  };
2179
2345
 
2180
2346
  // src/social/crypto/shares.ts
2181
- function xorBytes(a, b) {
2182
- if (a.length !== b.length) throw new Error("xorBytes: length mismatch");
2183
- const out = new Uint8Array(a.length);
2184
- for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
2185
- return out;
2186
- }
2347
+ import { secp256k1 } from "@noble/curves/secp256k1";
2348
+ import { split, combine } from "shamir-secret-sharing";
2187
2349
  function generateRandomBytes(n) {
2188
2350
  return crypto.getRandomValues(new Uint8Array(n));
2189
2351
  }
2190
- function generateWalletShares() {
2191
- const privateKey = generateRandomBytes(32);
2192
- const deviceShare = generateRandomBytes(32);
2193
- const serverShare = xorBytes(privateKey, deviceShare);
2194
- return { privateKey, deviceShare, serverShare };
2195
- }
2196
2352
  function zeroize(bytes) {
2197
2353
  bytes.fill(0);
2198
2354
  }
2355
+ function generateMaster() {
2356
+ let master;
2357
+ do {
2358
+ master = generateRandomBytes(32);
2359
+ } while (!secp256k1.utils.isValidPrivateKey(master));
2360
+ return master;
2361
+ }
2362
+ async function splitMaster(master, version = 1) {
2363
+ if (master.length !== 32) throw new Error("master must be 32 bytes");
2364
+ const parts = await split(master, 3, 2);
2365
+ if (parts.length !== 3) throw new Error("unexpected share count");
2366
+ return { device: parts[0], server: parts[1], recovery: parts[2], version };
2367
+ }
2199
2368
 
2200
2369
  // src/social/crypto/wrap.ts
2201
2370
  function buf(u) {
@@ -2240,6 +2409,8 @@ function validatePin(pin) {
2240
2409
  }
2241
2410
 
2242
2411
  // src/social/crypto/passkey.ts
2412
+ init_errors();
2413
+ import { startRegistration } from "@simplewebauthn/browser";
2243
2414
  function buf3(u) {
2244
2415
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2245
2416
  }
@@ -2247,30 +2418,24 @@ function isPasskeySupported() {
2247
2418
  return typeof window !== "undefined" && !!window.PublicKeyCredential && "isUserVerifyingPlatformAuthenticatorAvailable" in window.PublicKeyCredential;
2248
2419
  }
2249
2420
  async function registerPasskey(opts) {
2250
- const challenge = crypto.getRandomValues(new Uint8Array(32));
2251
- let cred;
2421
+ let response;
2252
2422
  try {
2253
- cred = await navigator.credentials.create({
2254
- publicKey: {
2255
- challenge: buf3(challenge),
2256
- rp: { id: opts.rpId, name: opts.rpName },
2257
- user: { id: buf3(opts.userId), name: opts.userName, displayName: opts.userName },
2258
- pubKeyCredParams: [
2259
- { type: "public-key", alg: -7 },
2260
- { type: "public-key", alg: -257 }
2261
- ],
2262
- authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
2263
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
2264
- }
2265
- });
2423
+ response = await startRegistration({ optionsJSON: opts.optionsJSON });
2266
2424
  } catch (e) {
2267
2425
  if (e.name === "NotAllowedError") throw new UserCancelledError();
2268
2426
  throw e;
2269
2427
  }
2270
- const ext = cred.getClientExtensionResults();
2271
- const prfResults = ext?.prf?.results;
2272
- const prfOutput = prfResults?.first ? new Uint8Array(prfResults.first) : void 0;
2273
- return { credentialId: new Uint8Array(cred.rawId), prfOutput };
2428
+ const extResults = response.clientExtensionResults;
2429
+ const prfResults = extResults?.prf?.results;
2430
+ let prfOutput;
2431
+ if (prfResults?.first) {
2432
+ prfOutput = typeof prfResults.first === "string" ? Uint8Array.from(atob(prfResults.first.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)) : new Uint8Array(prfResults.first);
2433
+ }
2434
+ const credentialId = Uint8Array.from(
2435
+ atob(response.id.replace(/-/g, "+").replace(/_/g, "/")),
2436
+ (c) => c.charCodeAt(0)
2437
+ );
2438
+ return { credentialId, prfOutput, response };
2274
2439
  }
2275
2440
  async function getPrfOutput(opts) {
2276
2441
  const challenge = crypto.getRandomValues(new Uint8Array(32));
@@ -2295,6 +2460,28 @@ async function getPrfOutput(opts) {
2295
2460
  return new Uint8Array(prfResults.first);
2296
2461
  }
2297
2462
 
2463
+ // src/social/webauthnRegistration.ts
2464
+ function bufFromBytes(u) {
2465
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2466
+ }
2467
+ async function registerWebAuthnCredential(opts) {
2468
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2469
+ const optionsJSON = {
2470
+ ...baseOptions,
2471
+ extensions: {
2472
+ ...baseOptions.extensions,
2473
+ prf: { eval: { first: bufFromBytes(opts.prfSalt) } }
2474
+ }
2475
+ };
2476
+ const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2477
+ try {
2478
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2479
+ } catch (e) {
2480
+ console.warn("decane-connect-kit: WebAuthn server-side registration failed; PRF-based unlock still works, but rotateShares()/exportPortableBackup() will not until this is retried.", e);
2481
+ }
2482
+ return { credentialId, prfOutput };
2483
+ }
2484
+
2298
2485
  // src/social/auth/email.ts
2299
2486
  async function startEmailAuth(api, email) {
2300
2487
  await api.authEmailStart(email);
@@ -2341,28 +2528,102 @@ async function fetchWrappedShareBackup(api, jwt) {
2341
2528
  return api.getRecoveryBackup(jwt);
2342
2529
  }
2343
2530
 
2344
- // src/social/recovery/phrase.ts
2345
- import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic } from "@scure/bip39";
2346
- import { wordlist } from "@scure/bip39/wordlists/english.js";
2347
- function shareToPhrase(deviceShare) {
2348
- if (deviceShare.length !== 32) throw new Error("deviceShare must be 32 bytes");
2349
- return entropyToMnemonic(deviceShare, wordlist);
2531
+ // src/social/recovery/recoveryShareFile.ts
2532
+ init_errors();
2533
+ var PBKDF2_ITERATIONS2 = 6e5;
2534
+ var FILE_VERSION = 2;
2535
+ function b642(u) {
2536
+ let s = "";
2537
+ for (const b of u) s += String.fromCharCode(b);
2538
+ return btoa(s);
2539
+ }
2540
+ function unb642(s) {
2541
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2542
+ }
2543
+ function buf4(u) {
2544
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2545
+ }
2546
+ async function deriveFileKey(password, salt, iterations) {
2547
+ const base = await crypto.subtle.importKey(
2548
+ "raw",
2549
+ new TextEncoder().encode(password),
2550
+ "PBKDF2",
2551
+ false,
2552
+ ["deriveKey"]
2553
+ );
2554
+ return crypto.subtle.deriveKey(
2555
+ { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
2556
+ base,
2557
+ { name: "AES-GCM", length: 256 },
2558
+ false,
2559
+ ["encrypt", "decrypt"]
2560
+ );
2561
+ }
2562
+ async function buildRecoveryShareFile(share, password, addresses, shareSetVersion, passwordHint) {
2563
+ if (share.length !== 33) throw new Error("share must be 33 bytes");
2564
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2565
+ if (passwordHint && passwordHint.trim() === password) {
2566
+ throw new Error("password hint must not be the password");
2567
+ }
2568
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2569
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2570
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
2571
+ const ct = new Uint8Array(
2572
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
2573
+ );
2574
+ return {
2575
+ version: FILE_VERSION,
2576
+ type: "decane-recovery-share",
2577
+ shareSetVersion,
2578
+ kdf: "pbkdf2-sha256",
2579
+ kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
2580
+ cipher: "aes-256-gcm",
2581
+ iv: b642(iv),
2582
+ ciphertext: b642(ct),
2583
+ addresses,
2584
+ passwordHint: passwordHint?.trim() || null,
2585
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2586
+ };
2587
+ }
2588
+ function parseRecoveryShareFile(json) {
2589
+ const raw = json;
2590
+ if (raw?.type === "decane-recovery") {
2591
+ throw new InvalidRecoveryFileError();
2592
+ }
2593
+ const f = json;
2594
+ if (!f || f.type !== "decane-recovery-share" || f.version !== FILE_VERSION || f.kdf !== "pbkdf2-sha256" || f.cipher !== "aes-256-gcm" || typeof f.iv !== "string" || typeof f.ciphertext !== "string" || typeof f.kdfParams?.salt !== "string" || typeof f.kdfParams?.iterations !== "number" || typeof f.shareSetVersion !== "number" || typeof f.addresses?.evm !== "string" || typeof f.addresses?.solana !== "string") {
2595
+ throw new InvalidRecoveryFileError();
2596
+ }
2597
+ if (f.kdfParams.iterations < 1e5 || f.kdfParams.iterations > 5e6) {
2598
+ throw new InvalidRecoveryFileError();
2599
+ }
2600
+ return f;
2350
2601
  }
2351
- function phraseToShare(phrase) {
2352
- const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
2353
- if (!validateMnemonic(normalized, wordlist)) throw new InvalidPhraseError();
2354
- const entropy = mnemonicToEntropy(normalized, wordlist);
2355
- if (entropy.length !== 32) throw new InvalidPhraseError();
2356
- return entropy;
2602
+ async function decryptRecoveryShareFile(file, password) {
2603
+ const salt = unb642(file.kdfParams.salt);
2604
+ const iv = unb642(file.iv);
2605
+ const ct = unb642(file.ciphertext);
2606
+ const key = await deriveFileKey(password, salt, file.kdfParams.iterations);
2607
+ let share;
2608
+ try {
2609
+ share = new Uint8Array(
2610
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
2611
+ );
2612
+ } catch {
2613
+ throw new WrongPasswordError();
2614
+ }
2615
+ if (share.length !== 33) {
2616
+ share.fill(0);
2617
+ throw new InvalidRecoveryFileError();
2618
+ }
2619
+ return share;
2357
2620
  }
2358
- function phraseConfirmationChallenge(phrase) {
2359
- const words = phrase.trim().split(/\s+/);
2360
- const chosen = /* @__PURE__ */ new Set();
2361
- while (chosen.size < 3) chosen.add(Math.floor(Math.random() * words.length));
2362
- return [...chosen].sort((a, b) => a - b).map((i) => ({ position: i + 1, expected: words[i] }));
2621
+ function recoveryShareFileName(evmAddress) {
2622
+ return `decane-recovery-${evmAddress.slice(0, 8)}.json`;
2363
2623
  }
2364
2624
 
2365
2625
  // src/social/recovery/recoveryFlow.ts
2626
+ init_errors();
2366
2627
  async function recoverDeviceShare(input) {
2367
2628
  const backup = await fetchWrappedShareBackup(input.api, input.jwt);
2368
2629
  if (backup) {
@@ -2396,37 +2657,87 @@ async function recoverDeviceShare(input) {
2396
2657
  });
2397
2658
  }
2398
2659
  }
2399
- if (!input.promptForPhrase) throw new NewDeviceError();
2400
- input.onProgress?.({ type: "phrase-prompt" });
2401
- const phrase = await input.promptForPhrase();
2402
- if (!phrase) throw new NewDeviceError();
2403
- let candidate;
2404
- try {
2405
- candidate = phraseToShare(phrase);
2406
- } catch {
2407
- throw new InvalidPhraseError();
2408
- }
2409
- input.onProgress?.({ type: "phrase-verifying" });
2660
+ if (!input.promptForRecoveryFile) throw new NewDeviceError();
2661
+ input.onProgress?.({ type: "recovery-file-prompt" });
2662
+ const supplied = await input.promptForRecoveryFile();
2663
+ if (!supplied) throw new NewDeviceError();
2664
+ const file = parseRecoveryShareFile(supplied.value);
2665
+ input.onProgress?.({ type: "password-prompt", passwordHint: file.passwordHint });
2666
+ const password = await supplied.getPassword();
2667
+ if (!password) throw new NewDeviceError();
2668
+ const recoveryShare = await decryptRecoveryShareFile(file, password);
2669
+ input.onProgress?.({ type: "recovery-verifying" });
2410
2670
  const tee = new TeeClient(input.teeUrl);
2411
- const verified = await tee.verifyRecoveryCandidate(candidate, input.jwt);
2412
- if (!verified) {
2413
- zeroize(candidate);
2414
- throw new RecoveryFailedError("phrase-mismatch");
2671
+ let result;
2672
+ try {
2673
+ result = await tee.recoverWithShare(input.jwt, recoveryShare);
2674
+ } finally {
2675
+ zeroize(recoveryShare);
2415
2676
  }
2416
- return candidate;
2677
+ input.onProgress?.({ type: "rotated", shareSetVersion: result.shareSetVersion });
2678
+ await input.onNewRecoveryShare?.(result.recoveryShare, result.shareSetVersion);
2679
+ zeroize(result.recoveryShare);
2680
+ return result.deviceShare;
2417
2681
  }
2418
2682
 
2419
- // src/social/recovery/phraseUi.ts
2420
- function buildPhraseResult(deviceShare) {
2421
- const phrase = shareToPhrase(deviceShare);
2422
- return { phrase, challenge: phraseConfirmationChallenge(phrase) };
2683
+ // src/social/recovery/portableBackup.ts
2684
+ init_errors();
2685
+ var PBKDF2_ITERATIONS3 = 6e5;
2686
+ var FILE_VERSION2 = 1;
2687
+ function b643(u) {
2688
+ let s = "";
2689
+ for (const b of u) s += String.fromCharCode(b);
2690
+ return btoa(s);
2423
2691
  }
2424
- function verifyConfirmationChallenge(challenge, userAnswers) {
2425
- if (userAnswers.length !== challenge.length) return false;
2426
- return challenge.every(({ position, expected }) => {
2427
- const answer = userAnswers.find((a) => a.position === position);
2428
- return !!answer && answer.word.trim().toLowerCase() === expected.toLowerCase();
2429
- });
2692
+ function buf5(u) {
2693
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2694
+ }
2695
+ async function deriveFileKey2(password, salt, iterations) {
2696
+ const base = await crypto.subtle.importKey(
2697
+ "raw",
2698
+ new TextEncoder().encode(password),
2699
+ "PBKDF2",
2700
+ false,
2701
+ ["deriveKey"]
2702
+ );
2703
+ return crypto.subtle.deriveKey(
2704
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
2705
+ base,
2706
+ { name: "AES-GCM", length: 256 },
2707
+ false,
2708
+ ["encrypt", "decrypt"]
2709
+ );
2710
+ }
2711
+ async function buildPortableBackup(deviceShare, recoveryShare, password, addresses, shareSetVersion, passwordHint) {
2712
+ if (deviceShare.length !== 33 || recoveryShare.length !== 33) {
2713
+ throw new Error("shares must be 33 bytes");
2714
+ }
2715
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2716
+ if (passwordHint && passwordHint.trim() === password) {
2717
+ throw new Error("password hint must not be the password");
2718
+ }
2719
+ const plaintext = new TextEncoder().encode(
2720
+ JSON.stringify({ deviceShare: b643(deviceShare), recoveryShare: b643(recoveryShare) })
2721
+ );
2722
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2723
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2724
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
2725
+ const ct = new Uint8Array(
2726
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
2727
+ );
2728
+ return {
2729
+ version: FILE_VERSION2,
2730
+ type: "decane-portable-backup",
2731
+ shareSetVersion,
2732
+ kdf: "pbkdf2-sha256",
2733
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
2734
+ cipher: "aes-256-gcm",
2735
+ iv: b643(iv),
2736
+ ciphertext: b643(ct),
2737
+ addresses,
2738
+ passwordHint: passwordHint?.trim() || null,
2739
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2740
+ };
2430
2741
  }
2431
2742
 
2432
2743
  // src/social/client.ts
@@ -2517,15 +2828,7 @@ async function createDecaneConnect(config) {
2517
2828
  let credentialId;
2518
2829
  let prfOutput;
2519
2830
  try {
2520
- const authType = config.authMethods[0] ?? "social";
2521
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2522
- const result = await registerPasskey({
2523
- rpId,
2524
- rpName,
2525
- userId: new TextEncoder().encode(userId),
2526
- userName: `decane-wallet-${authType}-${dateStr}`,
2527
- prfSalt
2528
- });
2831
+ const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2529
2832
  credentialId = result.credentialId;
2530
2833
  prfOutput = result.prfOutput;
2531
2834
  } catch (e) {
@@ -2564,15 +2867,7 @@ async function createDecaneConnect(config) {
2564
2867
  if (isPasskeySupported()) {
2565
2868
  const prfSalt = generateRandomBytes(32);
2566
2869
  try {
2567
- const authType = config.authMethods[0] ?? "social";
2568
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2569
- const { credentialId, prfOutput } = await registerPasskey({
2570
- rpId,
2571
- rpName,
2572
- userId: new TextEncoder().encode(userId),
2573
- userName: `decane-wallet-${authType}-${dateStr}`,
2574
- prfSalt
2575
- });
2870
+ const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2576
2871
  if (prfOutput) {
2577
2872
  unlockMethod = "passkey";
2578
2873
  wrappingKey = await importAesKey(prfOutput);
@@ -2595,19 +2890,21 @@ async function createDecaneConnect(config) {
2595
2890
  ({ wrappingKey, meta } = await pinSetup());
2596
2891
  unlockMethod = "pin";
2597
2892
  }
2598
- const { privateKey, deviceShare, serverShare } = generateWalletShares();
2893
+ const master = generateMaster();
2894
+ let shareSet;
2599
2895
  let evmAddress;
2600
2896
  let solanaAddress;
2601
2897
  try {
2602
- evmAddress = new Wallet(toHex(privateKey)).address;
2603
- solanaAddress = Keypair.fromSeed(privateKey).publicKey.toBase58();
2898
+ evmAddress = new Wallet(toHex(master)).address;
2899
+ solanaAddress = Keypair.fromSeed(master).publicKey.toBase58();
2900
+ shareSet = await splitMaster(master, 1);
2604
2901
  } finally {
2605
- zeroize(privateKey);
2902
+ zeroize(master);
2606
2903
  }
2607
2904
  const iv = generateRandomBytes(12);
2608
2905
  const wrappedDeviceShare = await aesGcmEncrypt(
2609
2906
  wrappingKey,
2610
- deviceShare,
2907
+ shareSet.device,
2611
2908
  iv
2612
2909
  );
2613
2910
  await storage.putShare({
@@ -2623,7 +2920,7 @@ async function createDecaneConnect(config) {
2623
2920
  });
2624
2921
  const serverIv = generateRandomBytes(12);
2625
2922
  await api.putShare(jwt, {
2626
- ciphertext: serverShare,
2923
+ ciphertext: shareSet.server,
2627
2924
  iv: serverIv,
2628
2925
  wrappedShareMeta: {
2629
2926
  unlockMethod
@@ -2632,7 +2929,7 @@ async function createDecaneConnect(config) {
2632
2929
  evmAddress,
2633
2930
  solanaAddress
2634
2931
  });
2635
- zeroize(serverShare);
2932
+ zeroize(shareSet.server);
2636
2933
  const addresses = { evm: evmAddress, solana: solanaAddress };
2637
2934
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
2638
2935
  try {
@@ -2647,19 +2944,27 @@ async function createDecaneConnect(config) {
2647
2944
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
2648
2945
  }
2649
2946
  }
2650
- await session.start(deviceShare, jwt, addresses);
2651
- if (config.offerPhraseAtSignup !== false && config.onPhraseGenerated) {
2947
+ await session.start(shareSet.device, jwt, addresses);
2948
+ zeroize(shareSet.device);
2949
+ if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
2652
2950
  try {
2653
- const result = buildPhraseResult(deviceShare);
2654
- const { userConfirmed, userAnswers } = await config.onPhraseGenerated(result);
2655
- if (userConfirmed && verifyConfirmationChallenge(result.challenge, userAnswers)) {
2656
- await api.markPhraseSaved(jwt);
2951
+ const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
2952
+ if (choice.wants) {
2953
+ const file = await buildRecoveryShareFile(
2954
+ shareSet.recovery,
2955
+ choice.password,
2956
+ addresses,
2957
+ shareSet.version,
2958
+ choice.passwordHint
2959
+ );
2960
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
2961
+ await api.markRecoveryShareSaved(jwt);
2657
2962
  }
2658
2963
  } catch (e) {
2659
- console.warn("decane-connect-kit: recovery phrase offer failed or was skipped.", e);
2964
+ console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
2660
2965
  }
2661
2966
  }
2662
- zeroize(deviceShare);
2967
+ zeroize(shareSet.recovery);
2663
2968
  return addresses;
2664
2969
  }
2665
2970
  async function returningUserUnlock(userId, jwt) {
@@ -2681,7 +2986,17 @@ async function createDecaneConnect(config) {
2681
2986
  api,
2682
2987
  storage,
2683
2988
  onProgress: config.onRecoveryProgress,
2684
- promptForPhrase: config.promptForRecoveryPhrase
2989
+ promptForRecoveryFile: config.promptForRecoveryFile,
2990
+ 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));
2998
+ await api.markRecoveryShareSaved(jwt);
2999
+ }
2685
3000
  });
2686
3001
  if (!await storage.getShare(userId) && isPasskeySupported()) {
2687
3002
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
@@ -2856,23 +3171,82 @@ async function createDecaneConnect(config) {
2856
3171
  getAddresses() {
2857
3172
  return auth?.addresses ?? persisted?.addresses ?? null;
2858
3173
  },
2859
- async generateRecoveryPhrase() {
3174
+ async rotateShares(opts) {
2860
3175
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2861
- const stored = await storage.getShare(auth.userId);
2862
- if (!stored) throw new NewDeviceError();
2863
- const wrappingKey = await getWrappingKey(stored);
2864
- const deviceShare = await aesGcmDecrypt(wrappingKey, stored.wrappedDeviceShare, stored.iv);
3176
+ const tee = session.requireActive();
3177
+ const result = await tee.rotateShares(auth.jwt);
3178
+ try {
3179
+ const file = await buildRecoveryShareFile(
3180
+ result.recoveryShare,
3181
+ opts.password,
3182
+ auth.addresses,
3183
+ result.shareSetVersion,
3184
+ opts.passwordHint
3185
+ );
3186
+ await api.markRecoveryShareSaved(auth.jwt);
3187
+ return file;
3188
+ } finally {
3189
+ zeroize(result.deviceShare);
3190
+ zeroize(result.recoveryShare);
3191
+ }
3192
+ },
3193
+ async exportPortableBackup(opts) {
3194
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3195
+ const tee = session.requireActive();
3196
+ const result = await tee.exportPortableBackup(auth.jwt);
2865
3197
  try {
2866
- return buildPhraseResult(deviceShare);
3198
+ return await buildPortableBackup(
3199
+ result.deviceShare,
3200
+ result.recoveryShare,
3201
+ opts.password,
3202
+ auth.addresses,
3203
+ result.shareSetVersion,
3204
+ opts.passwordHint
3205
+ );
2867
3206
  } finally {
2868
- zeroize(deviceShare);
3207
+ zeroize(result.deviceShare);
3208
+ zeroize(result.recoveryShare);
2869
3209
  }
2870
3210
  },
2871
- async markPhraseSaved(challenge, userAnswers) {
3211
+ async hasRecoveryShare() {
2872
3212
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2873
- if (!verifyConfirmationChallenge(challenge, userAnswers)) return false;
2874
- await api.markPhraseSaved(auth.jwt);
2875
- return true;
3213
+ const status = await api.getRecoveryStatus(auth.jwt);
3214
+ return status.hasRecoveryShare;
3215
+ },
3216
+ async shouldPromptRecovery() {
3217
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3218
+ const status = await api.getRecoveryStatus(auth.jwt);
3219
+ if (status.hasRecoveryShare || status.nagged) return false;
3220
+ try {
3221
+ const checks = [];
3222
+ if (auth.addresses.evm) {
3223
+ const { JsonRpcProvider } = await import("ethers");
3224
+ for (const chain of config.chains.filter((c) => c.startsWith("evm:"))) {
3225
+ const rpcUrl = config.rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
3226
+ if (!rpcUrl) continue;
3227
+ checks.push(
3228
+ new JsonRpcProvider(rpcUrl).getBalance(auth.addresses.evm).then((b) => b > 0n).catch(() => false)
3229
+ );
3230
+ }
3231
+ }
3232
+ if (auth.addresses.solana && config.chains.some((c) => c.startsWith("solana:"))) {
3233
+ const { Connection, PublicKey } = await import("@solana/web3.js");
3234
+ const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3235
+ const cluster = solChain.slice("solana:".length);
3236
+ const endpoint = cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
3237
+ checks.push(
3238
+ new Connection(endpoint).getBalance(new PublicKey(auth.addresses.solana)).then((b) => b > 0).catch(() => false)
3239
+ );
3240
+ }
3241
+ const results = await Promise.all(checks);
3242
+ return results.some(Boolean);
3243
+ } catch {
3244
+ return false;
3245
+ }
3246
+ },
3247
+ async dismissRecoveryPrompt() {
3248
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3249
+ await api.dismissRecoveryNag(auth.jwt);
2876
3250
  },
2877
3251
  async hasBackup() {
2878
3252
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
@@ -2887,17 +3261,20 @@ async function createDecaneConnect(config) {
2887
3261
  };
2888
3262
  return client;
2889
3263
  }
2890
- function bufferToB64u(buf4) {
2891
- return btoa(String.fromCharCode(...buf4)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3264
+ function bufferToB64u(buf6) {
3265
+ return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2892
3266
  }
2893
3267
  function b64uToBuffer(b64u) {
2894
- const b642 = b64u.replace(/-/g, "+").replace(/_/g, "/");
2895
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
3268
+ const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3269
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2896
3270
  }
2897
3271
  function toHex(bytes) {
2898
3272
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2899
3273
  }
2900
3274
 
3275
+ // src/social/index.ts
3276
+ init_errors();
3277
+
2901
3278
  // src/ui/WalletSelector.tsx
2902
3279
  import React2, { useEffect, useCallback, useState } from "react";
2903
3280
  import { createPortal } from "react-dom";
@@ -4676,14 +5053,16 @@ export {
4676
5053
  DecaneError,
4677
5054
  DecaneErrorCode,
4678
5055
  DecaneKit,
5056
+ ExportNotAuthorizedError,
4679
5057
  InvalidApiKeyError,
4680
- InvalidPhraseError,
4681
5058
  InvalidPinError,
5059
+ InvalidRecoveryFileError,
4682
5060
  NetworkError,
4683
5061
  NewDeviceError,
4684
5062
  OriginNotAllowedError,
4685
5063
  PromptPinNotConfiguredError,
4686
5064
  RecoveryFailedError,
5065
+ RecoveryWalletMismatchError,
4687
5066
  SessionExpiredError,
4688
5067
  ShareCorruptedError,
4689
5068
  SocialWalletModal,
@@ -4691,6 +5070,7 @@ export {
4691
5070
  UserCancelledError,
4692
5071
  WalletSelector,
4693
5072
  WebAuthnNotSupportedError,
5073
+ WrongPasswordError,
4694
5074
  createDecane,
4695
5075
  createDecaneConnect,
4696
5076
  startEmailAuth,