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