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.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,138 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/social/errors.ts
34
+ var errors_exports = {};
35
+ __export(errors_exports, {
36
+ AttestationError: () => AttestationError,
37
+ CallSequenceError: () => CallSequenceError,
38
+ DecaneConnectError: () => DecaneConnectError,
39
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
40
+ InvalidApiKeyError: () => InvalidApiKeyError,
41
+ InvalidPinError: () => InvalidPinError,
42
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
43
+ NetworkError: () => NetworkError,
44
+ NewDeviceError: () => NewDeviceError,
45
+ OriginNotAllowedError: () => OriginNotAllowedError,
46
+ PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
47
+ RecoveryFailedError: () => RecoveryFailedError,
48
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
49
+ SessionExpiredError: () => SessionExpiredError,
50
+ ShareCorruptedError: () => ShareCorruptedError,
51
+ UnlockRequiredError: () => UnlockRequiredError,
52
+ UserCancelledError: () => UserCancelledError,
53
+ WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
54
+ WrongPasswordError: () => WrongPasswordError
55
+ });
56
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
57
+ var init_errors = __esm({
58
+ "src/social/errors.ts"() {
59
+ "use strict";
60
+ DecaneConnectError = class extends Error {
61
+ constructor(code, message, retryable = false) {
62
+ super(message);
63
+ this.name = "DecaneConnectError";
64
+ this.code = code;
65
+ this.retryable = retryable;
66
+ }
67
+ };
68
+ InvalidApiKeyError = class extends DecaneConnectError {
69
+ constructor() {
70
+ super("INVALID_API_KEY", "API key is invalid or revoked");
71
+ }
72
+ };
73
+ OriginNotAllowedError = class extends DecaneConnectError {
74
+ constructor(origin) {
75
+ super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
76
+ }
77
+ };
78
+ SessionExpiredError = class extends DecaneConnectError {
79
+ constructor() {
80
+ super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
81
+ }
82
+ };
83
+ UnlockRequiredError = class extends DecaneConnectError {
84
+ constructor() {
85
+ super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
86
+ }
87
+ };
88
+ UserCancelledError = class extends DecaneConnectError {
89
+ constructor() {
90
+ super("USER_CANCELLED", "User cancelled the operation", true);
91
+ }
92
+ };
93
+ WebAuthnNotSupportedError = class extends DecaneConnectError {
94
+ constructor() {
95
+ super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
96
+ }
97
+ };
98
+ InvalidPinError = class extends DecaneConnectError {
99
+ constructor() {
100
+ super("INVALID_PIN", "Incorrect PIN", true);
101
+ }
102
+ };
103
+ CallSequenceError = class extends DecaneConnectError {
104
+ constructor(msg) {
105
+ super("CALL_SEQUENCE", msg);
106
+ }
107
+ };
108
+ NetworkError = class extends DecaneConnectError {
109
+ constructor() {
110
+ super("NETWORK", "Network request failed", true);
111
+ }
112
+ };
113
+ ShareCorruptedError = class extends DecaneConnectError {
114
+ constructor() {
115
+ super("SHARE_CORRUPTED", "Stored device share is corrupted");
116
+ }
117
+ };
118
+ NewDeviceError = class extends DecaneConnectError {
119
+ constructor() {
120
+ super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
121
+ }
122
+ };
123
+ PromptPinNotConfiguredError = class extends DecaneConnectError {
124
+ constructor() {
125
+ super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
126
+ }
127
+ };
128
+ AttestationError = class extends DecaneConnectError {
129
+ constructor(msg) {
130
+ super("ATTESTATION_FAILED", msg);
131
+ }
132
+ };
133
+ RecoveryFailedError = class extends DecaneConnectError {
134
+ constructor(reason) {
135
+ super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
136
+ }
137
+ };
138
+ InvalidRecoveryFileError = class extends DecaneConnectError {
139
+ constructor() {
140
+ super("INVALID_RECOVERY_FILE", "This does not look like a valid recovery file", false);
141
+ }
142
+ };
143
+ WrongPasswordError = class extends DecaneConnectError {
144
+ constructor() {
145
+ super("WRONG_PASSWORD", "Incorrect password for this recovery file", true);
146
+ }
147
+ };
148
+ RecoveryWalletMismatchError = class extends DecaneConnectError {
149
+ constructor() {
150
+ super(
151
+ "RECOVERY_WALLET_MISMATCH",
152
+ "This recovery file is for a different wallet than the account you are signed into",
153
+ false
154
+ );
155
+ }
156
+ };
157
+ ExportNotAuthorizedError = class extends DecaneConnectError {
158
+ constructor() {
159
+ super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
160
+ }
161
+ };
162
+ }
163
+ });
164
+
30
165
  // src/index.ts
31
166
  var index_exports = {};
32
167
  __export(index_exports, {
@@ -38,14 +173,16 @@ __export(index_exports, {
38
173
  DecaneError: () => DecaneError,
39
174
  DecaneErrorCode: () => DecaneErrorCode,
40
175
  DecaneKit: () => DecaneKit,
176
+ ExportNotAuthorizedError: () => ExportNotAuthorizedError,
41
177
  InvalidApiKeyError: () => InvalidApiKeyError,
42
- InvalidPhraseError: () => InvalidPhraseError,
43
178
  InvalidPinError: () => InvalidPinError,
179
+ InvalidRecoveryFileError: () => InvalidRecoveryFileError,
44
180
  NetworkError: () => NetworkError,
45
181
  NewDeviceError: () => NewDeviceError,
46
182
  OriginNotAllowedError: () => OriginNotAllowedError,
47
183
  PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
48
184
  RecoveryFailedError: () => RecoveryFailedError,
185
+ RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
49
186
  SessionExpiredError: () => SessionExpiredError,
50
187
  ShareCorruptedError: () => ShareCorruptedError,
51
188
  SocialWalletModal: () => SocialWalletModal,
@@ -53,6 +190,7 @@ __export(index_exports, {
53
190
  UserCancelledError: () => UserCancelledError,
54
191
  WalletSelector: () => WalletSelector,
55
192
  WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
193
+ WrongPasswordError: () => WrongPasswordError,
56
194
  createDecane: () => createDecane,
57
195
  createDecaneConnect: () => createDecaneConnect,
58
196
  startEmailAuth: () => startEmailAuth,
@@ -1806,91 +1944,7 @@ function createDecane(config = {}) {
1806
1944
  // src/social/client.ts
1807
1945
  var import_ethers2 = require("ethers");
1808
1946
  var import_web32 = require("@solana/web3.js");
1809
-
1810
- // src/social/errors.ts
1811
- var DecaneConnectError = class extends Error {
1812
- constructor(code, message, retryable = false) {
1813
- super(message);
1814
- this.name = "DecaneConnectError";
1815
- this.code = code;
1816
- this.retryable = retryable;
1817
- }
1818
- };
1819
- var InvalidApiKeyError = class extends DecaneConnectError {
1820
- constructor() {
1821
- super("INVALID_API_KEY", "API key is invalid or revoked");
1822
- }
1823
- };
1824
- var OriginNotAllowedError = class extends DecaneConnectError {
1825
- constructor(origin) {
1826
- super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
1827
- }
1828
- };
1829
- var SessionExpiredError = class extends DecaneConnectError {
1830
- constructor() {
1831
- super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
1832
- }
1833
- };
1834
- var UnlockRequiredError = class extends DecaneConnectError {
1835
- constructor() {
1836
- super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
1837
- }
1838
- };
1839
- var UserCancelledError = class extends DecaneConnectError {
1840
- constructor() {
1841
- super("USER_CANCELLED", "User cancelled the operation", true);
1842
- }
1843
- };
1844
- var WebAuthnNotSupportedError = class extends DecaneConnectError {
1845
- constructor() {
1846
- super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
1847
- }
1848
- };
1849
- var InvalidPinError = class extends DecaneConnectError {
1850
- constructor() {
1851
- super("INVALID_PIN", "Incorrect PIN", true);
1852
- }
1853
- };
1854
- var CallSequenceError = class extends DecaneConnectError {
1855
- constructor(msg) {
1856
- super("CALL_SEQUENCE", msg);
1857
- }
1858
- };
1859
- var NetworkError = class extends DecaneConnectError {
1860
- constructor() {
1861
- super("NETWORK", "Network request failed", true);
1862
- }
1863
- };
1864
- var ShareCorruptedError = class extends DecaneConnectError {
1865
- constructor() {
1866
- super("SHARE_CORRUPTED", "Stored device share is corrupted");
1867
- }
1868
- };
1869
- var NewDeviceError = class extends DecaneConnectError {
1870
- constructor() {
1871
- super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
1872
- }
1873
- };
1874
- var PromptPinNotConfiguredError = class extends DecaneConnectError {
1875
- constructor() {
1876
- super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
1877
- }
1878
- };
1879
- var AttestationError = class extends DecaneConnectError {
1880
- constructor(msg) {
1881
- super("ATTESTATION_FAILED", msg);
1882
- }
1883
- };
1884
- var RecoveryFailedError = class extends DecaneConnectError {
1885
- constructor(reason) {
1886
- super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
1887
- }
1888
- };
1889
- var InvalidPhraseError = class extends DecaneConnectError {
1890
- constructor() {
1891
- super("INVALID_PHRASE", "Recovery phrase is invalid or corrupted", true);
1892
- }
1893
- };
1947
+ init_errors();
1894
1948
 
1895
1949
  // src/social/events.ts
1896
1950
  var EventEmitter = class {
@@ -1910,6 +1964,7 @@ var EventEmitter = class {
1910
1964
  };
1911
1965
 
1912
1966
  // src/social/api.ts
1967
+ init_errors();
1913
1968
  var ERROR_MAP = {
1914
1969
  INVALID_API_KEY: InvalidApiKeyError,
1915
1970
  ORIGIN_NOT_ALLOWED: OriginNotAllowedError
@@ -1980,13 +2035,26 @@ var ApiClient = class {
1980
2035
  })
1981
2036
  });
1982
2037
  }
2038
+ // ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
2039
+ async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
2040
+ return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
2041
+ headers: { Authorization: `Bearer ${jwt}` }
2042
+ });
2043
+ }
2044
+ async postWebAuthnRegister(jwt, response, origin) {
2045
+ await this._request("/webauthn/register", {
2046
+ method: "POST",
2047
+ headers: { Authorization: `Bearer ${jwt}` },
2048
+ body: JSON.stringify({ response, origin })
2049
+ });
2050
+ }
1983
2051
  async revoke(jwt) {
1984
2052
  await this._request("/auth/revoke", {
1985
2053
  method: "POST",
1986
2054
  headers: { Authorization: `Bearer ${jwt}` }
1987
2055
  });
1988
2056
  }
1989
- // ── Recovery (passkey-sync backup + phrase-saved flag) ────────────────────
2057
+ // ── Recovery (passkey-sync backup + recovery-share-saved flag) ────────────
1990
2058
  async hasRecoveryBackup(jwt) {
1991
2059
  const data = await this._request("/recovery/backup/exists", {
1992
2060
  headers: { Authorization: `Bearer ${jwt}` }
@@ -2027,8 +2095,19 @@ var ApiClient = class {
2027
2095
  headers: { Authorization: `Bearer ${jwt}` }
2028
2096
  });
2029
2097
  }
2030
- async markPhraseSaved(jwt) {
2031
- await this._request("/recovery/phrase-saved", {
2098
+ async markRecoveryShareSaved(jwt) {
2099
+ await this._request("/recovery/share-saved", {
2100
+ method: "POST",
2101
+ headers: { Authorization: `Bearer ${jwt}` }
2102
+ });
2103
+ }
2104
+ async getRecoveryStatus(jwt) {
2105
+ return this._request("/recovery/status", {
2106
+ headers: { Authorization: `Bearer ${jwt}` }
2107
+ });
2108
+ }
2109
+ async dismissRecoveryNag(jwt) {
2110
+ await this._request("/recovery/nag-dismissed", {
2032
2111
  method: "POST",
2033
2112
  headers: { Authorization: `Bearer ${jwt}` }
2034
2113
  });
@@ -2037,8 +2116,8 @@ var ApiClient = class {
2037
2116
  function bytesToB64(bytes) {
2038
2117
  return btoa(String.fromCharCode(...bytes));
2039
2118
  }
2040
- function b64ToBytes(b642) {
2041
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
2119
+ function b64ToBytes(b644) {
2120
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2042
2121
  }
2043
2122
 
2044
2123
  // src/social/storage.ts
@@ -2064,8 +2143,12 @@ var Storage = class {
2064
2143
  }
2065
2144
  };
2066
2145
 
2146
+ // src/social/teeClient.ts
2147
+ var import_browser = require("@simplewebauthn/browser");
2148
+
2067
2149
  // src/social/attestation.ts
2068
2150
  var import_dcap_qvl = require("@phala/dcap-qvl");
2151
+ init_errors();
2069
2152
 
2070
2153
  // ../../infra/tee-measurement.json
2071
2154
  var tee_measurement_default = {
@@ -2123,11 +2206,15 @@ async function verifyTeeAttestation(teeUrl, nonce) {
2123
2206
  }
2124
2207
 
2125
2208
  // src/social/teeClient.ts
2209
+ init_errors();
2126
2210
  function bytesToBase64(b) {
2127
2211
  let s = "";
2128
2212
  for (const byte of b) s += String.fromCharCode(byte);
2129
2213
  return btoa(s);
2130
2214
  }
2215
+ function base64ToBytes(s) {
2216
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2217
+ }
2131
2218
  var TeeClient = class {
2132
2219
  constructor(teeUrl) {
2133
2220
  this.teeUrl = teeUrl;
@@ -2151,16 +2238,90 @@ var TeeClient = class {
2151
2238
  if (!res.ok) throw new Error("session open failed");
2152
2239
  this.handle = (await res.json()).handle;
2153
2240
  }
2154
- /** Verify a candidate device share (from a recovery phrase) without exposing shares. */
2155
- async verifyRecoveryCandidate(deviceShare, jwt) {
2241
+ /**
2242
+ * Flow B: exchange a recovery share (from a recovery-share file) for a
2243
+ * fresh device share. The server verifies the reconstructed master
2244
+ * actually matches this account's addresses before returning anything —
2245
+ * see the security note in the TEE's /signer/recover handler. Rotates the
2246
+ * whole share set: the caller MUST persist the returned recoveryShare as a
2247
+ * new recovery-share file, since the old one stops working immediately.
2248
+ */
2249
+ async recoverWithShare(jwt, recoveryShare) {
2156
2250
  await this.ensureAttested();
2157
- const res = await fetch(`${this.teeUrl}/signer/verify-recovery`, {
2251
+ const res = await fetch(`${this.teeUrl}/signer/recover`, {
2158
2252
  method: "POST",
2159
2253
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2160
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2254
+ body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
2255
+ });
2256
+ if (res.status === 403) {
2257
+ const { RecoveryWalletMismatchError: RecoveryWalletMismatchError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2258
+ throw new RecoveryWalletMismatchError2();
2259
+ }
2260
+ if (!res.ok) throw new Error("recovery failed");
2261
+ const body = await res.json();
2262
+ return {
2263
+ deviceShare: base64ToBytes(body.deviceShare),
2264
+ recoveryShare: base64ToBytes(body.recoveryShare),
2265
+ shareSetVersion: body.shareSetVersion
2266
+ };
2267
+ }
2268
+ /**
2269
+ * Request a fresh WebAuthn assertion challenge and complete it in the
2270
+ * browser via @simplewebauthn/browser's startAuthentication(), which
2271
+ * handles all the base64url (de)serialization the raw navigator.credentials
2272
+ * API requires. Shared by rotateShares() and exportPortableBackup() — both
2273
+ * require proof of fresh authenticator possession, not just an active session.
2274
+ */
2275
+ async completeAssertion(jwt) {
2276
+ const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
2277
+ method: "POST",
2278
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2279
+ body: JSON.stringify({ rpId: window.location.hostname })
2161
2280
  });
2162
- if (!res.ok) return false;
2163
- return (await res.json()).verified === true;
2281
+ if (!res.ok) throw new ExportNotAuthorizedError();
2282
+ const { challengeId, options } = await res.json();
2283
+ let assertion;
2284
+ try {
2285
+ assertion = await (0, import_browser.startAuthentication)({ optionsJSON: options });
2286
+ } catch {
2287
+ const { UserCancelledError: UserCancelledError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
2288
+ throw new UserCancelledError2();
2289
+ }
2290
+ return { challengeId, assertion };
2291
+ }
2292
+ /** Voluntary rotation — e.g. after a suspected device compromise, or to invalidate an old recovery file. */
2293
+ async rotateShares(jwt) {
2294
+ if (!this.handle) throw new Error("no session");
2295
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2296
+ const res = await fetch(`${this.teeUrl}/signer/rotate`, {
2297
+ method: "POST",
2298
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2299
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2300
+ });
2301
+ if (!res.ok) throw new ExportNotAuthorizedError();
2302
+ const body = await res.json();
2303
+ return {
2304
+ deviceShare: base64ToBytes(body.deviceShare),
2305
+ recoveryShare: base64ToBytes(body.recoveryShare),
2306
+ shareSetVersion: body.shareSetVersion
2307
+ };
2308
+ }
2309
+ /** The two-share bearer portable backup — see recovery/portableBackup.ts. Rotates the set on export. */
2310
+ async exportPortableBackup(jwt) {
2311
+ if (!this.handle) throw new Error("no session");
2312
+ const { challengeId, assertion } = await this.completeAssertion(jwt);
2313
+ const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
2314
+ method: "POST",
2315
+ headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2316
+ body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
2317
+ });
2318
+ if (!res.ok) throw new ExportNotAuthorizedError();
2319
+ const body = await res.json();
2320
+ return {
2321
+ deviceShare: base64ToBytes(body.deviceShare),
2322
+ recoveryShare: base64ToBytes(body.recoveryShare),
2323
+ shareSetVersion: body.shareSetVersion
2324
+ };
2164
2325
  }
2165
2326
  hasSession() {
2166
2327
  return this.handle !== null;
@@ -2193,6 +2354,7 @@ var TeeClient = class {
2193
2354
  };
2194
2355
 
2195
2356
  // src/social/session.ts
2357
+ init_errors();
2196
2358
  var SessionManager = class {
2197
2359
  constructor(events, durationMs, teeUrl) {
2198
2360
  this.events = events;
@@ -2248,24 +2410,27 @@ var SessionManager = class {
2248
2410
  };
2249
2411
 
2250
2412
  // src/social/crypto/shares.ts
2251
- function xorBytes(a, b) {
2252
- if (a.length !== b.length) throw new Error("xorBytes: length mismatch");
2253
- const out = new Uint8Array(a.length);
2254
- for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
2255
- return out;
2256
- }
2413
+ var import_secp256k1 = require("@noble/curves/secp256k1");
2414
+ var import_shamir_secret_sharing = require("shamir-secret-sharing");
2257
2415
  function generateRandomBytes(n) {
2258
2416
  return crypto.getRandomValues(new Uint8Array(n));
2259
2417
  }
2260
- function generateWalletShares() {
2261
- const privateKey = generateRandomBytes(32);
2262
- const deviceShare = generateRandomBytes(32);
2263
- const serverShare = xorBytes(privateKey, deviceShare);
2264
- return { privateKey, deviceShare, serverShare };
2265
- }
2266
2418
  function zeroize(bytes) {
2267
2419
  bytes.fill(0);
2268
2420
  }
2421
+ function generateMaster() {
2422
+ let master;
2423
+ do {
2424
+ master = generateRandomBytes(32);
2425
+ } while (!import_secp256k1.secp256k1.utils.isValidPrivateKey(master));
2426
+ return master;
2427
+ }
2428
+ async function splitMaster(master, version = 1) {
2429
+ if (master.length !== 32) throw new Error("master must be 32 bytes");
2430
+ const parts = await (0, import_shamir_secret_sharing.split)(master, 3, 2);
2431
+ if (parts.length !== 3) throw new Error("unexpected share count");
2432
+ return { device: parts[0], server: parts[1], recovery: parts[2], version };
2433
+ }
2269
2434
 
2270
2435
  // src/social/crypto/wrap.ts
2271
2436
  function buf(u) {
@@ -2310,6 +2475,8 @@ function validatePin(pin) {
2310
2475
  }
2311
2476
 
2312
2477
  // src/social/crypto/passkey.ts
2478
+ var import_browser2 = require("@simplewebauthn/browser");
2479
+ init_errors();
2313
2480
  function buf3(u) {
2314
2481
  return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2315
2482
  }
@@ -2317,30 +2484,24 @@ function isPasskeySupported() {
2317
2484
  return typeof window !== "undefined" && !!window.PublicKeyCredential && "isUserVerifyingPlatformAuthenticatorAvailable" in window.PublicKeyCredential;
2318
2485
  }
2319
2486
  async function registerPasskey(opts) {
2320
- const challenge = crypto.getRandomValues(new Uint8Array(32));
2321
- let cred;
2487
+ let response;
2322
2488
  try {
2323
- cred = await navigator.credentials.create({
2324
- publicKey: {
2325
- challenge: buf3(challenge),
2326
- rp: { id: opts.rpId, name: opts.rpName },
2327
- user: { id: buf3(opts.userId), name: opts.userName, displayName: opts.userName },
2328
- pubKeyCredParams: [
2329
- { type: "public-key", alg: -7 },
2330
- { type: "public-key", alg: -257 }
2331
- ],
2332
- authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
2333
- extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
2334
- }
2335
- });
2489
+ response = await (0, import_browser2.startRegistration)({ optionsJSON: opts.optionsJSON });
2336
2490
  } catch (e) {
2337
2491
  if (e.name === "NotAllowedError") throw new UserCancelledError();
2338
2492
  throw e;
2339
2493
  }
2340
- const ext = cred.getClientExtensionResults();
2341
- const prfResults = ext?.prf?.results;
2342
- const prfOutput = prfResults?.first ? new Uint8Array(prfResults.first) : void 0;
2343
- return { credentialId: new Uint8Array(cred.rawId), prfOutput };
2494
+ const extResults = response.clientExtensionResults;
2495
+ const prfResults = extResults?.prf?.results;
2496
+ let prfOutput;
2497
+ if (prfResults?.first) {
2498
+ prfOutput = typeof prfResults.first === "string" ? Uint8Array.from(atob(prfResults.first.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)) : new Uint8Array(prfResults.first);
2499
+ }
2500
+ const credentialId = Uint8Array.from(
2501
+ atob(response.id.replace(/-/g, "+").replace(/_/g, "/")),
2502
+ (c) => c.charCodeAt(0)
2503
+ );
2504
+ return { credentialId, prfOutput, response };
2344
2505
  }
2345
2506
  async function getPrfOutput(opts) {
2346
2507
  const challenge = crypto.getRandomValues(new Uint8Array(32));
@@ -2365,6 +2526,28 @@ async function getPrfOutput(opts) {
2365
2526
  return new Uint8Array(prfResults.first);
2366
2527
  }
2367
2528
 
2529
+ // src/social/webauthnRegistration.ts
2530
+ function bufFromBytes(u) {
2531
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2532
+ }
2533
+ async function registerWebAuthnCredential(opts) {
2534
+ const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
2535
+ const optionsJSON = {
2536
+ ...baseOptions,
2537
+ extensions: {
2538
+ ...baseOptions.extensions,
2539
+ prf: { eval: { first: bufFromBytes(opts.prfSalt) } }
2540
+ }
2541
+ };
2542
+ const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
2543
+ try {
2544
+ await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
2545
+ } catch (e) {
2546
+ 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);
2547
+ }
2548
+ return { credentialId, prfOutput };
2549
+ }
2550
+
2368
2551
  // src/social/auth/email.ts
2369
2552
  async function startEmailAuth(api, email) {
2370
2553
  await api.authEmailStart(email);
@@ -2411,28 +2594,102 @@ async function fetchWrappedShareBackup(api, jwt) {
2411
2594
  return api.getRecoveryBackup(jwt);
2412
2595
  }
2413
2596
 
2414
- // src/social/recovery/phrase.ts
2415
- var import_bip39 = require("@scure/bip39");
2416
- var import_english = require("@scure/bip39/wordlists/english.js");
2417
- function shareToPhrase(deviceShare) {
2418
- if (deviceShare.length !== 32) throw new Error("deviceShare must be 32 bytes");
2419
- return (0, import_bip39.entropyToMnemonic)(deviceShare, import_english.wordlist);
2597
+ // src/social/recovery/recoveryShareFile.ts
2598
+ init_errors();
2599
+ var PBKDF2_ITERATIONS2 = 6e5;
2600
+ var FILE_VERSION = 2;
2601
+ function b642(u) {
2602
+ let s = "";
2603
+ for (const b of u) s += String.fromCharCode(b);
2604
+ return btoa(s);
2605
+ }
2606
+ function unb642(s) {
2607
+ return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2608
+ }
2609
+ function buf4(u) {
2610
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2611
+ }
2612
+ async function deriveFileKey(password, salt, iterations) {
2613
+ const base = await crypto.subtle.importKey(
2614
+ "raw",
2615
+ new TextEncoder().encode(password),
2616
+ "PBKDF2",
2617
+ false,
2618
+ ["deriveKey"]
2619
+ );
2620
+ return crypto.subtle.deriveKey(
2621
+ { name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
2622
+ base,
2623
+ { name: "AES-GCM", length: 256 },
2624
+ false,
2625
+ ["encrypt", "decrypt"]
2626
+ );
2627
+ }
2628
+ async function buildRecoveryShareFile(share, password, addresses, shareSetVersion, passwordHint) {
2629
+ if (share.length !== 33) throw new Error("share must be 33 bytes");
2630
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2631
+ if (passwordHint && passwordHint.trim() === password) {
2632
+ throw new Error("password hint must not be the password");
2633
+ }
2634
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2635
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2636
+ const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
2637
+ const ct = new Uint8Array(
2638
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
2639
+ );
2640
+ return {
2641
+ version: FILE_VERSION,
2642
+ type: "decane-recovery-share",
2643
+ shareSetVersion,
2644
+ kdf: "pbkdf2-sha256",
2645
+ kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
2646
+ cipher: "aes-256-gcm",
2647
+ iv: b642(iv),
2648
+ ciphertext: b642(ct),
2649
+ addresses,
2650
+ passwordHint: passwordHint?.trim() || null,
2651
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2652
+ };
2420
2653
  }
2421
- function phraseToShare(phrase) {
2422
- const normalized = phrase.trim().toLowerCase().split(/\s+/).join(" ");
2423
- if (!(0, import_bip39.validateMnemonic)(normalized, import_english.wordlist)) throw new InvalidPhraseError();
2424
- const entropy = (0, import_bip39.mnemonicToEntropy)(normalized, import_english.wordlist);
2425
- if (entropy.length !== 32) throw new InvalidPhraseError();
2426
- return entropy;
2654
+ function parseRecoveryShareFile(json) {
2655
+ const raw = json;
2656
+ if (raw?.type === "decane-recovery") {
2657
+ throw new InvalidRecoveryFileError();
2658
+ }
2659
+ const f = json;
2660
+ 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") {
2661
+ throw new InvalidRecoveryFileError();
2662
+ }
2663
+ if (f.kdfParams.iterations < 1e5 || f.kdfParams.iterations > 5e6) {
2664
+ throw new InvalidRecoveryFileError();
2665
+ }
2666
+ return f;
2667
+ }
2668
+ async function decryptRecoveryShareFile(file, password) {
2669
+ const salt = unb642(file.kdfParams.salt);
2670
+ const iv = unb642(file.iv);
2671
+ const ct = unb642(file.ciphertext);
2672
+ const key = await deriveFileKey(password, salt, file.kdfParams.iterations);
2673
+ let share;
2674
+ try {
2675
+ share = new Uint8Array(
2676
+ await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
2677
+ );
2678
+ } catch {
2679
+ throw new WrongPasswordError();
2680
+ }
2681
+ if (share.length !== 33) {
2682
+ share.fill(0);
2683
+ throw new InvalidRecoveryFileError();
2684
+ }
2685
+ return share;
2427
2686
  }
2428
- function phraseConfirmationChallenge(phrase) {
2429
- const words = phrase.trim().split(/\s+/);
2430
- const chosen = /* @__PURE__ */ new Set();
2431
- while (chosen.size < 3) chosen.add(Math.floor(Math.random() * words.length));
2432
- return [...chosen].sort((a, b) => a - b).map((i) => ({ position: i + 1, expected: words[i] }));
2687
+ function recoveryShareFileName(evmAddress) {
2688
+ return `decane-recovery-${evmAddress.slice(0, 8)}.json`;
2433
2689
  }
2434
2690
 
2435
2691
  // src/social/recovery/recoveryFlow.ts
2692
+ init_errors();
2436
2693
  async function recoverDeviceShare(input) {
2437
2694
  const backup = await fetchWrappedShareBackup(input.api, input.jwt);
2438
2695
  if (backup) {
@@ -2466,37 +2723,87 @@ async function recoverDeviceShare(input) {
2466
2723
  });
2467
2724
  }
2468
2725
  }
2469
- if (!input.promptForPhrase) throw new NewDeviceError();
2470
- input.onProgress?.({ type: "phrase-prompt" });
2471
- const phrase = await input.promptForPhrase();
2472
- if (!phrase) throw new NewDeviceError();
2473
- let candidate;
2474
- try {
2475
- candidate = phraseToShare(phrase);
2476
- } catch {
2477
- throw new InvalidPhraseError();
2478
- }
2479
- input.onProgress?.({ type: "phrase-verifying" });
2726
+ if (!input.promptForRecoveryFile) throw new NewDeviceError();
2727
+ input.onProgress?.({ type: "recovery-file-prompt" });
2728
+ const supplied = await input.promptForRecoveryFile();
2729
+ if (!supplied) throw new NewDeviceError();
2730
+ const file = parseRecoveryShareFile(supplied.value);
2731
+ input.onProgress?.({ type: "password-prompt", passwordHint: file.passwordHint });
2732
+ const password = await supplied.getPassword();
2733
+ if (!password) throw new NewDeviceError();
2734
+ const recoveryShare = await decryptRecoveryShareFile(file, password);
2735
+ input.onProgress?.({ type: "recovery-verifying" });
2480
2736
  const tee = new TeeClient(input.teeUrl);
2481
- const verified = await tee.verifyRecoveryCandidate(candidate, input.jwt);
2482
- if (!verified) {
2483
- zeroize(candidate);
2484
- throw new RecoveryFailedError("phrase-mismatch");
2737
+ let result;
2738
+ try {
2739
+ result = await tee.recoverWithShare(input.jwt, recoveryShare);
2740
+ } finally {
2741
+ zeroize(recoveryShare);
2485
2742
  }
2486
- return candidate;
2743
+ input.onProgress?.({ type: "rotated", shareSetVersion: result.shareSetVersion });
2744
+ await input.onNewRecoveryShare?.(result.recoveryShare, result.shareSetVersion);
2745
+ zeroize(result.recoveryShare);
2746
+ return result.deviceShare;
2487
2747
  }
2488
2748
 
2489
- // src/social/recovery/phraseUi.ts
2490
- function buildPhraseResult(deviceShare) {
2491
- const phrase = shareToPhrase(deviceShare);
2492
- return { phrase, challenge: phraseConfirmationChallenge(phrase) };
2749
+ // src/social/recovery/portableBackup.ts
2750
+ init_errors();
2751
+ var PBKDF2_ITERATIONS3 = 6e5;
2752
+ var FILE_VERSION2 = 1;
2753
+ function b643(u) {
2754
+ let s = "";
2755
+ for (const b of u) s += String.fromCharCode(b);
2756
+ return btoa(s);
2493
2757
  }
2494
- function verifyConfirmationChallenge(challenge, userAnswers) {
2495
- if (userAnswers.length !== challenge.length) return false;
2496
- return challenge.every(({ position, expected }) => {
2497
- const answer = userAnswers.find((a) => a.position === position);
2498
- return !!answer && answer.word.trim().toLowerCase() === expected.toLowerCase();
2499
- });
2758
+ function buf5(u) {
2759
+ return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
2760
+ }
2761
+ async function deriveFileKey2(password, salt, iterations) {
2762
+ const base = await crypto.subtle.importKey(
2763
+ "raw",
2764
+ new TextEncoder().encode(password),
2765
+ "PBKDF2",
2766
+ false,
2767
+ ["deriveKey"]
2768
+ );
2769
+ return crypto.subtle.deriveKey(
2770
+ { name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
2771
+ base,
2772
+ { name: "AES-GCM", length: 256 },
2773
+ false,
2774
+ ["encrypt", "decrypt"]
2775
+ );
2776
+ }
2777
+ async function buildPortableBackup(deviceShare, recoveryShare, password, addresses, shareSetVersion, passwordHint) {
2778
+ if (deviceShare.length !== 33 || recoveryShare.length !== 33) {
2779
+ throw new Error("shares must be 33 bytes");
2780
+ }
2781
+ if (password.length < 8) throw new Error("password must be at least 8 characters");
2782
+ if (passwordHint && passwordHint.trim() === password) {
2783
+ throw new Error("password hint must not be the password");
2784
+ }
2785
+ const plaintext = new TextEncoder().encode(
2786
+ JSON.stringify({ deviceShare: b643(deviceShare), recoveryShare: b643(recoveryShare) })
2787
+ );
2788
+ const salt = crypto.getRandomValues(new Uint8Array(16));
2789
+ const iv = crypto.getRandomValues(new Uint8Array(12));
2790
+ const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
2791
+ const ct = new Uint8Array(
2792
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
2793
+ );
2794
+ return {
2795
+ version: FILE_VERSION2,
2796
+ type: "decane-portable-backup",
2797
+ shareSetVersion,
2798
+ kdf: "pbkdf2-sha256",
2799
+ kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
2800
+ cipher: "aes-256-gcm",
2801
+ iv: b643(iv),
2802
+ ciphertext: b643(ct),
2803
+ addresses,
2804
+ passwordHint: passwordHint?.trim() || null,
2805
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2806
+ };
2500
2807
  }
2501
2808
 
2502
2809
  // src/social/client.ts
@@ -2587,15 +2894,7 @@ async function createDecaneConnect(config) {
2587
2894
  let credentialId;
2588
2895
  let prfOutput;
2589
2896
  try {
2590
- const authType = config.authMethods[0] ?? "social";
2591
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2592
- const result = await registerPasskey({
2593
- rpId,
2594
- rpName,
2595
- userId: new TextEncoder().encode(userId),
2596
- userName: `decane-wallet-${authType}-${dateStr}`,
2597
- prfSalt
2598
- });
2897
+ const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2599
2898
  credentialId = result.credentialId;
2600
2899
  prfOutput = result.prfOutput;
2601
2900
  } catch (e) {
@@ -2634,15 +2933,7 @@ async function createDecaneConnect(config) {
2634
2933
  if (isPasskeySupported()) {
2635
2934
  const prfSalt = generateRandomBytes(32);
2636
2935
  try {
2637
- const authType = config.authMethods[0] ?? "social";
2638
- const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2639
- const { credentialId, prfOutput } = await registerPasskey({
2640
- rpId,
2641
- rpName,
2642
- userId: new TextEncoder().encode(userId),
2643
- userName: `decane-wallet-${authType}-${dateStr}`,
2644
- prfSalt
2645
- });
2936
+ const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
2646
2937
  if (prfOutput) {
2647
2938
  unlockMethod = "passkey";
2648
2939
  wrappingKey = await importAesKey(prfOutput);
@@ -2665,19 +2956,21 @@ async function createDecaneConnect(config) {
2665
2956
  ({ wrappingKey, meta } = await pinSetup());
2666
2957
  unlockMethod = "pin";
2667
2958
  }
2668
- const { privateKey, deviceShare, serverShare } = generateWalletShares();
2959
+ const master = generateMaster();
2960
+ let shareSet;
2669
2961
  let evmAddress;
2670
2962
  let solanaAddress;
2671
2963
  try {
2672
- evmAddress = new import_ethers2.Wallet(toHex(privateKey)).address;
2673
- solanaAddress = import_web32.Keypair.fromSeed(privateKey).publicKey.toBase58();
2964
+ evmAddress = new import_ethers2.Wallet(toHex(master)).address;
2965
+ solanaAddress = import_web32.Keypair.fromSeed(master).publicKey.toBase58();
2966
+ shareSet = await splitMaster(master, 1);
2674
2967
  } finally {
2675
- zeroize(privateKey);
2968
+ zeroize(master);
2676
2969
  }
2677
2970
  const iv = generateRandomBytes(12);
2678
2971
  const wrappedDeviceShare = await aesGcmEncrypt(
2679
2972
  wrappingKey,
2680
- deviceShare,
2973
+ shareSet.device,
2681
2974
  iv
2682
2975
  );
2683
2976
  await storage.putShare({
@@ -2693,7 +2986,7 @@ async function createDecaneConnect(config) {
2693
2986
  });
2694
2987
  const serverIv = generateRandomBytes(12);
2695
2988
  await api.putShare(jwt, {
2696
- ciphertext: serverShare,
2989
+ ciphertext: shareSet.server,
2697
2990
  iv: serverIv,
2698
2991
  wrappedShareMeta: {
2699
2992
  unlockMethod
@@ -2702,7 +2995,7 @@ async function createDecaneConnect(config) {
2702
2995
  evmAddress,
2703
2996
  solanaAddress
2704
2997
  });
2705
- zeroize(serverShare);
2998
+ zeroize(shareSet.server);
2706
2999
  const addresses = { evm: evmAddress, solana: solanaAddress };
2707
3000
  if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
2708
3001
  try {
@@ -2717,19 +3010,27 @@ async function createDecaneConnect(config) {
2717
3010
  console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
2718
3011
  }
2719
3012
  }
2720
- await session.start(deviceShare, jwt, addresses);
2721
- if (config.offerPhraseAtSignup !== false && config.onPhraseGenerated) {
3013
+ await session.start(shareSet.device, jwt, addresses);
3014
+ zeroize(shareSet.device);
3015
+ if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
2722
3016
  try {
2723
- const result = buildPhraseResult(deviceShare);
2724
- const { userConfirmed, userAnswers } = await config.onPhraseGenerated(result);
2725
- if (userConfirmed && verifyConfirmationChallenge(result.challenge, userAnswers)) {
2726
- await api.markPhraseSaved(jwt);
3017
+ const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
3018
+ if (choice.wants) {
3019
+ const file = await buildRecoveryShareFile(
3020
+ shareSet.recovery,
3021
+ choice.password,
3022
+ addresses,
3023
+ shareSet.version,
3024
+ choice.passwordHint
3025
+ );
3026
+ await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
3027
+ await api.markRecoveryShareSaved(jwt);
2727
3028
  }
2728
3029
  } catch (e) {
2729
- console.warn("decane-connect-kit: recovery phrase offer failed or was skipped.", e);
3030
+ console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
2730
3031
  }
2731
3032
  }
2732
- zeroize(deviceShare);
3033
+ zeroize(shareSet.recovery);
2733
3034
  return addresses;
2734
3035
  }
2735
3036
  async function returningUserUnlock(userId, jwt) {
@@ -2751,7 +3052,17 @@ async function createDecaneConnect(config) {
2751
3052
  api,
2752
3053
  storage,
2753
3054
  onProgress: config.onRecoveryProgress,
2754
- promptForPhrase: config.promptForRecoveryPhrase
3055
+ promptForRecoveryFile: config.promptForRecoveryFile,
3056
+ 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));
3064
+ await api.markRecoveryShareSaved(jwt);
3065
+ }
2755
3066
  });
2756
3067
  if (!await storage.getShare(userId) && isPasskeySupported()) {
2757
3068
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
@@ -2926,23 +3237,82 @@ async function createDecaneConnect(config) {
2926
3237
  getAddresses() {
2927
3238
  return auth?.addresses ?? persisted?.addresses ?? null;
2928
3239
  },
2929
- async generateRecoveryPhrase() {
3240
+ async rotateShares(opts) {
2930
3241
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2931
- const stored = await storage.getShare(auth.userId);
2932
- if (!stored) throw new NewDeviceError();
2933
- const wrappingKey = await getWrappingKey(stored);
2934
- const deviceShare = await aesGcmDecrypt(wrappingKey, stored.wrappedDeviceShare, stored.iv);
3242
+ const tee = session.requireActive();
3243
+ const result = await tee.rotateShares(auth.jwt);
3244
+ try {
3245
+ const file = await buildRecoveryShareFile(
3246
+ result.recoveryShare,
3247
+ opts.password,
3248
+ auth.addresses,
3249
+ result.shareSetVersion,
3250
+ opts.passwordHint
3251
+ );
3252
+ await api.markRecoveryShareSaved(auth.jwt);
3253
+ return file;
3254
+ } finally {
3255
+ zeroize(result.deviceShare);
3256
+ zeroize(result.recoveryShare);
3257
+ }
3258
+ },
3259
+ async exportPortableBackup(opts) {
3260
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3261
+ const tee = session.requireActive();
3262
+ const result = await tee.exportPortableBackup(auth.jwt);
2935
3263
  try {
2936
- return buildPhraseResult(deviceShare);
3264
+ return await buildPortableBackup(
3265
+ result.deviceShare,
3266
+ result.recoveryShare,
3267
+ opts.password,
3268
+ auth.addresses,
3269
+ result.shareSetVersion,
3270
+ opts.passwordHint
3271
+ );
2937
3272
  } finally {
2938
- zeroize(deviceShare);
3273
+ zeroize(result.deviceShare);
3274
+ zeroize(result.recoveryShare);
3275
+ }
3276
+ },
3277
+ async hasRecoveryShare() {
3278
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3279
+ const status = await api.getRecoveryStatus(auth.jwt);
3280
+ return status.hasRecoveryShare;
3281
+ },
3282
+ async shouldPromptRecovery() {
3283
+ if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
3284
+ const status = await api.getRecoveryStatus(auth.jwt);
3285
+ if (status.hasRecoveryShare || status.nagged) return false;
3286
+ try {
3287
+ const checks = [];
3288
+ if (auth.addresses.evm) {
3289
+ const { JsonRpcProvider } = await import("ethers");
3290
+ for (const chain of config.chains.filter((c) => c.startsWith("evm:"))) {
3291
+ const rpcUrl = config.rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
3292
+ if (!rpcUrl) continue;
3293
+ checks.push(
3294
+ new JsonRpcProvider(rpcUrl).getBalance(auth.addresses.evm).then((b) => b > 0n).catch(() => false)
3295
+ );
3296
+ }
3297
+ }
3298
+ if (auth.addresses.solana && config.chains.some((c) => c.startsWith("solana:"))) {
3299
+ const { Connection, PublicKey } = await import("@solana/web3.js");
3300
+ const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3301
+ const cluster = solChain.slice("solana:".length);
3302
+ const endpoint = cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
3303
+ checks.push(
3304
+ new Connection(endpoint).getBalance(new PublicKey(auth.addresses.solana)).then((b) => b > 0).catch(() => false)
3305
+ );
3306
+ }
3307
+ const results = await Promise.all(checks);
3308
+ return results.some(Boolean);
3309
+ } catch {
3310
+ return false;
2939
3311
  }
2940
3312
  },
2941
- async markPhraseSaved(challenge, userAnswers) {
3313
+ async dismissRecoveryPrompt() {
2942
3314
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
2943
- if (!verifyConfirmationChallenge(challenge, userAnswers)) return false;
2944
- await api.markPhraseSaved(auth.jwt);
2945
- return true;
3315
+ await api.dismissRecoveryNag(auth.jwt);
2946
3316
  },
2947
3317
  async hasBackup() {
2948
3318
  if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
@@ -2957,17 +3327,20 @@ async function createDecaneConnect(config) {
2957
3327
  };
2958
3328
  return client;
2959
3329
  }
2960
- function bufferToB64u(buf4) {
2961
- return btoa(String.fromCharCode(...buf4)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3330
+ function bufferToB64u(buf6) {
3331
+ return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2962
3332
  }
2963
3333
  function b64uToBuffer(b64u) {
2964
- const b642 = b64u.replace(/-/g, "+").replace(/_/g, "/");
2965
- return Uint8Array.from(atob(b642), (c) => c.charCodeAt(0));
3334
+ const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
3335
+ return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2966
3336
  }
2967
3337
  function toHex(bytes) {
2968
3338
  return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2969
3339
  }
2970
3340
 
3341
+ // src/social/index.ts
3342
+ init_errors();
3343
+
2971
3344
  // src/ui/WalletSelector.tsx
2972
3345
  var import_react2 = __toESM(require("react"), 1);
2973
3346
  var import_react_dom = require("react-dom");
@@ -4740,14 +5113,16 @@ function useSignTypedData() {
4740
5113
  DecaneError,
4741
5114
  DecaneErrorCode,
4742
5115
  DecaneKit,
5116
+ ExportNotAuthorizedError,
4743
5117
  InvalidApiKeyError,
4744
- InvalidPhraseError,
4745
5118
  InvalidPinError,
5119
+ InvalidRecoveryFileError,
4746
5120
  NetworkError,
4747
5121
  NewDeviceError,
4748
5122
  OriginNotAllowedError,
4749
5123
  PromptPinNotConfiguredError,
4750
5124
  RecoveryFailedError,
5125
+ RecoveryWalletMismatchError,
4751
5126
  SessionExpiredError,
4752
5127
  ShareCorruptedError,
4753
5128
  SocialWalletModal,
@@ -4755,6 +5130,7 @@ function useSignTypedData() {
4755
5130
  UserCancelledError,
4756
5131
  WalletSelector,
4757
5132
  WebAuthnNotSupportedError,
5133
+ WrongPasswordError,
4758
5134
  createDecane,
4759
5135
  createDecaneConnect,
4760
5136
  startEmailAuth,