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.js
CHANGED
|
@@ -1,3 +1,155 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/social/errors.ts
|
|
12
|
+
var errors_exports = {};
|
|
13
|
+
__export(errors_exports, {
|
|
14
|
+
AttestationError: () => AttestationError,
|
|
15
|
+
CallSequenceError: () => CallSequenceError,
|
|
16
|
+
DecaneConnectError: () => DecaneConnectError,
|
|
17
|
+
ExportNotAuthorizedError: () => ExportNotAuthorizedError,
|
|
18
|
+
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
19
|
+
InvalidPinError: () => InvalidPinError,
|
|
20
|
+
InvalidRecoveryFileError: () => InvalidRecoveryFileError,
|
|
21
|
+
NetworkError: () => NetworkError,
|
|
22
|
+
NewDeviceError: () => NewDeviceError,
|
|
23
|
+
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
24
|
+
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
25
|
+
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
26
|
+
RecoveryFailedError: () => RecoveryFailedError,
|
|
27
|
+
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
28
|
+
SessionExpiredError: () => SessionExpiredError,
|
|
29
|
+
ShareCorruptedError: () => ShareCorruptedError,
|
|
30
|
+
UnlockRequiredError: () => UnlockRequiredError,
|
|
31
|
+
UserCancelledError: () => UserCancelledError,
|
|
32
|
+
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
33
|
+
WrongPasswordError: () => WrongPasswordError
|
|
34
|
+
});
|
|
35
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
36
|
+
var init_errors = __esm({
|
|
37
|
+
"src/social/errors.ts"() {
|
|
38
|
+
"use strict";
|
|
39
|
+
DecaneConnectError = class extends Error {
|
|
40
|
+
constructor(code, message, retryable = false) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "DecaneConnectError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.retryable = retryable;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
InvalidApiKeyError = class extends DecaneConnectError {
|
|
48
|
+
constructor() {
|
|
49
|
+
super("INVALID_API_KEY", "API key is invalid or revoked");
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
OriginNotAllowedError = class extends DecaneConnectError {
|
|
53
|
+
constructor(origin) {
|
|
54
|
+
super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
SessionExpiredError = class extends DecaneConnectError {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
UnlockRequiredError = class extends DecaneConnectError {
|
|
63
|
+
constructor() {
|
|
64
|
+
super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
ReconnectRequiredError = class extends DecaneConnectError {
|
|
68
|
+
constructor() {
|
|
69
|
+
super(
|
|
70
|
+
"RECONNECT_REQUIRED",
|
|
71
|
+
"Your session ended \u2014 sign in again to unlock this wallet",
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
UserCancelledError = class extends DecaneConnectError {
|
|
77
|
+
constructor() {
|
|
78
|
+
super("USER_CANCELLED", "User cancelled the operation", true);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
WebAuthnNotSupportedError = class extends DecaneConnectError {
|
|
82
|
+
constructor() {
|
|
83
|
+
super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
InvalidPinError = class extends DecaneConnectError {
|
|
87
|
+
constructor() {
|
|
88
|
+
super("INVALID_PIN", "Incorrect PIN", true);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
CallSequenceError = class extends DecaneConnectError {
|
|
92
|
+
constructor(msg) {
|
|
93
|
+
super("CALL_SEQUENCE", msg);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
NetworkError = class extends DecaneConnectError {
|
|
97
|
+
constructor() {
|
|
98
|
+
super("NETWORK", "Network request failed", true);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
ShareCorruptedError = class extends DecaneConnectError {
|
|
102
|
+
constructor() {
|
|
103
|
+
super("SHARE_CORRUPTED", "Stored device share is corrupted");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
NewDeviceError = class extends DecaneConnectError {
|
|
107
|
+
constructor() {
|
|
108
|
+
super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
PromptPinNotConfiguredError = class extends DecaneConnectError {
|
|
112
|
+
constructor() {
|
|
113
|
+
super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
AttestationError = class extends DecaneConnectError {
|
|
117
|
+
constructor(msg) {
|
|
118
|
+
super("ATTESTATION_FAILED", msg);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
RecoveryFailedError = class extends DecaneConnectError {
|
|
122
|
+
constructor(reason) {
|
|
123
|
+
super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
InvalidRecoveryFileError = class extends DecaneConnectError {
|
|
127
|
+
constructor() {
|
|
128
|
+
super("INVALID_RECOVERY_FILE", "This does not look like a valid recovery file", false);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
WrongPasswordError = class extends DecaneConnectError {
|
|
132
|
+
constructor() {
|
|
133
|
+
super("WRONG_PASSWORD", "Incorrect password for this recovery file", true);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
RecoveryWalletMismatchError = class extends DecaneConnectError {
|
|
137
|
+
constructor() {
|
|
138
|
+
super(
|
|
139
|
+
"RECOVERY_WALLET_MISMATCH",
|
|
140
|
+
"This recovery file is for a different wallet than the account you are signed into",
|
|
141
|
+
false
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
ExportNotAuthorizedError = class extends DecaneConnectError {
|
|
146
|
+
constructor() {
|
|
147
|
+
super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
1
153
|
// src/core/emitter.ts
|
|
2
154
|
function createEmitter() {
|
|
3
155
|
const listeners = /* @__PURE__ */ new Map();
|
|
@@ -1734,94 +1886,10 @@ function createDecane(config = {}) {
|
|
|
1734
1886
|
}
|
|
1735
1887
|
|
|
1736
1888
|
// src/social/client.ts
|
|
1889
|
+
init_errors();
|
|
1737
1890
|
import { Wallet } from "ethers";
|
|
1738
1891
|
import { Transaction as Transaction2, VersionedTransaction, Keypair } from "@solana/web3.js";
|
|
1739
1892
|
|
|
1740
|
-
// src/social/errors.ts
|
|
1741
|
-
var DecaneConnectError = class extends Error {
|
|
1742
|
-
constructor(code, message, retryable = false) {
|
|
1743
|
-
super(message);
|
|
1744
|
-
this.name = "DecaneConnectError";
|
|
1745
|
-
this.code = code;
|
|
1746
|
-
this.retryable = retryable;
|
|
1747
|
-
}
|
|
1748
|
-
};
|
|
1749
|
-
var InvalidApiKeyError = class extends DecaneConnectError {
|
|
1750
|
-
constructor() {
|
|
1751
|
-
super("INVALID_API_KEY", "API key is invalid or revoked");
|
|
1752
|
-
}
|
|
1753
|
-
};
|
|
1754
|
-
var OriginNotAllowedError = class extends DecaneConnectError {
|
|
1755
|
-
constructor(origin) {
|
|
1756
|
-
super("ORIGIN_NOT_ALLOWED", `Origin ${origin} not in API key allowlist`);
|
|
1757
|
-
}
|
|
1758
|
-
};
|
|
1759
|
-
var SessionExpiredError = class extends DecaneConnectError {
|
|
1760
|
-
constructor() {
|
|
1761
|
-
super("SESSION_EXPIRED", "Session has expired \u2014 call unlock()", true);
|
|
1762
|
-
}
|
|
1763
|
-
};
|
|
1764
|
-
var UnlockRequiredError = class extends DecaneConnectError {
|
|
1765
|
-
constructor() {
|
|
1766
|
-
super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
|
|
1767
|
-
}
|
|
1768
|
-
};
|
|
1769
|
-
var UserCancelledError = class extends DecaneConnectError {
|
|
1770
|
-
constructor() {
|
|
1771
|
-
super("USER_CANCELLED", "User cancelled the operation", true);
|
|
1772
|
-
}
|
|
1773
|
-
};
|
|
1774
|
-
var WebAuthnNotSupportedError = class extends DecaneConnectError {
|
|
1775
|
-
constructor() {
|
|
1776
|
-
super("WEBAUTHN_UNSUPPORTED", "This browser does not support WebAuthn");
|
|
1777
|
-
}
|
|
1778
|
-
};
|
|
1779
|
-
var InvalidPinError = class extends DecaneConnectError {
|
|
1780
|
-
constructor() {
|
|
1781
|
-
super("INVALID_PIN", "Incorrect PIN", true);
|
|
1782
|
-
}
|
|
1783
|
-
};
|
|
1784
|
-
var CallSequenceError = class extends DecaneConnectError {
|
|
1785
|
-
constructor(msg) {
|
|
1786
|
-
super("CALL_SEQUENCE", msg);
|
|
1787
|
-
}
|
|
1788
|
-
};
|
|
1789
|
-
var NetworkError = class extends DecaneConnectError {
|
|
1790
|
-
constructor() {
|
|
1791
|
-
super("NETWORK", "Network request failed", true);
|
|
1792
|
-
}
|
|
1793
|
-
};
|
|
1794
|
-
var ShareCorruptedError = class extends DecaneConnectError {
|
|
1795
|
-
constructor() {
|
|
1796
|
-
super("SHARE_CORRUPTED", "Stored device share is corrupted");
|
|
1797
|
-
}
|
|
1798
|
-
};
|
|
1799
|
-
var NewDeviceError = class extends DecaneConnectError {
|
|
1800
|
-
constructor() {
|
|
1801
|
-
super("NEW_DEVICE", "No device share found. Re-authenticate to set up this device.");
|
|
1802
|
-
}
|
|
1803
|
-
};
|
|
1804
|
-
var PromptPinNotConfiguredError = class extends DecaneConnectError {
|
|
1805
|
-
constructor() {
|
|
1806
|
-
super("PROMPT_PIN_NOT_CONFIGURED", "Passkey unavailable and no promptPin callback was provided in config");
|
|
1807
|
-
}
|
|
1808
|
-
};
|
|
1809
|
-
var AttestationError = class extends DecaneConnectError {
|
|
1810
|
-
constructor(msg) {
|
|
1811
|
-
super("ATTESTATION_FAILED", msg);
|
|
1812
|
-
}
|
|
1813
|
-
};
|
|
1814
|
-
var RecoveryFailedError = class extends DecaneConnectError {
|
|
1815
|
-
constructor(reason) {
|
|
1816
|
-
super("RECOVERY_FAILED", `Recovery failed: ${reason}`, true);
|
|
1817
|
-
}
|
|
1818
|
-
};
|
|
1819
|
-
var InvalidPhraseError = class extends DecaneConnectError {
|
|
1820
|
-
constructor() {
|
|
1821
|
-
super("INVALID_PHRASE", "Recovery phrase is invalid or corrupted", true);
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
|
|
1825
1893
|
// src/social/events.ts
|
|
1826
1894
|
var EventEmitter = class {
|
|
1827
1895
|
constructor() {
|
|
@@ -1840,6 +1908,7 @@ var EventEmitter = class {
|
|
|
1840
1908
|
};
|
|
1841
1909
|
|
|
1842
1910
|
// src/social/api.ts
|
|
1911
|
+
init_errors();
|
|
1843
1912
|
var ERROR_MAP = {
|
|
1844
1913
|
INVALID_API_KEY: InvalidApiKeyError,
|
|
1845
1914
|
ORIGIN_NOT_ALLOWED: OriginNotAllowedError
|
|
@@ -1910,13 +1979,26 @@ var ApiClient = class {
|
|
|
1910
1979
|
})
|
|
1911
1980
|
});
|
|
1912
1981
|
}
|
|
1982
|
+
// ── WebAuthn registration (additive to the PRF-only passkey flow) ─────────
|
|
1983
|
+
async getWebAuthnRegisterOptions(jwt, rpId, rpName) {
|
|
1984
|
+
return this._request(`/webauthn/register-options?rpId=${encodeURIComponent(rpId)}&rpName=${encodeURIComponent(rpName)}`, {
|
|
1985
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async postWebAuthnRegister(jwt, response, origin) {
|
|
1989
|
+
await this._request("/webauthn/register", {
|
|
1990
|
+
method: "POST",
|
|
1991
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
1992
|
+
body: JSON.stringify({ response, origin })
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1913
1995
|
async revoke(jwt) {
|
|
1914
1996
|
await this._request("/auth/revoke", {
|
|
1915
1997
|
method: "POST",
|
|
1916
1998
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
1917
1999
|
});
|
|
1918
2000
|
}
|
|
1919
|
-
// ── Recovery (passkey-sync backup +
|
|
2001
|
+
// ── Recovery (passkey-sync backup + recovery-share-saved flag) ────────────
|
|
1920
2002
|
async hasRecoveryBackup(jwt) {
|
|
1921
2003
|
const data = await this._request("/recovery/backup/exists", {
|
|
1922
2004
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
@@ -1957,8 +2039,19 @@ var ApiClient = class {
|
|
|
1957
2039
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
1958
2040
|
});
|
|
1959
2041
|
}
|
|
1960
|
-
async
|
|
1961
|
-
await this._request("/recovery/
|
|
2042
|
+
async markRecoveryShareSaved(jwt) {
|
|
2043
|
+
await this._request("/recovery/share-saved", {
|
|
2044
|
+
method: "POST",
|
|
2045
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
async getRecoveryStatus(jwt) {
|
|
2049
|
+
return this._request("/recovery/status", {
|
|
2050
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
async dismissRecoveryNag(jwt) {
|
|
2054
|
+
await this._request("/recovery/nag-dismissed", {
|
|
1962
2055
|
method: "POST",
|
|
1963
2056
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
1964
2057
|
});
|
|
@@ -1967,8 +2060,8 @@ var ApiClient = class {
|
|
|
1967
2060
|
function bytesToB64(bytes) {
|
|
1968
2061
|
return btoa(String.fromCharCode(...bytes));
|
|
1969
2062
|
}
|
|
1970
|
-
function b64ToBytes(
|
|
1971
|
-
return Uint8Array.from(atob(
|
|
2063
|
+
function b64ToBytes(b644) {
|
|
2064
|
+
return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
|
|
1972
2065
|
}
|
|
1973
2066
|
|
|
1974
2067
|
// src/social/storage.ts
|
|
@@ -1994,7 +2087,11 @@ var Storage = class {
|
|
|
1994
2087
|
}
|
|
1995
2088
|
};
|
|
1996
2089
|
|
|
2090
|
+
// src/social/teeClient.ts
|
|
2091
|
+
import { startAuthentication } from "@simplewebauthn/browser";
|
|
2092
|
+
|
|
1997
2093
|
// src/social/attestation.ts
|
|
2094
|
+
init_errors();
|
|
1998
2095
|
import { getCollateralAndVerify } from "@phala/dcap-qvl";
|
|
1999
2096
|
|
|
2000
2097
|
// ../../infra/tee-measurement.json
|
|
@@ -2053,11 +2150,15 @@ async function verifyTeeAttestation(teeUrl, nonce) {
|
|
|
2053
2150
|
}
|
|
2054
2151
|
|
|
2055
2152
|
// src/social/teeClient.ts
|
|
2153
|
+
init_errors();
|
|
2056
2154
|
function bytesToBase64(b) {
|
|
2057
2155
|
let s = "";
|
|
2058
2156
|
for (const byte of b) s += String.fromCharCode(byte);
|
|
2059
2157
|
return btoa(s);
|
|
2060
2158
|
}
|
|
2159
|
+
function base64ToBytes(s) {
|
|
2160
|
+
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
2161
|
+
}
|
|
2061
2162
|
var TeeClient = class {
|
|
2062
2163
|
constructor(teeUrl) {
|
|
2063
2164
|
this.teeUrl = teeUrl;
|
|
@@ -2071,6 +2172,13 @@ var TeeClient = class {
|
|
|
2071
2172
|
await verifyTeeAttestation(this.teeUrl, nonceHex);
|
|
2072
2173
|
this.attested = true;
|
|
2073
2174
|
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Opens a signing session and returns the addresses the TEE derived from the
|
|
2177
|
+
* key it actually reconstructed — authoritative, unlike the backend's
|
|
2178
|
+
* recorded copy, which nothing else can verify. Returns null if the TEE
|
|
2179
|
+
* couldn't derive them (older signer build, or a transient share-fetch
|
|
2180
|
+
* failure); callers fall back to the backend's addresses in that case.
|
|
2181
|
+
*/
|
|
2074
2182
|
async openSession(deviceShare, jwt) {
|
|
2075
2183
|
await this.ensureAttested();
|
|
2076
2184
|
const res = await fetch(`${this.teeUrl}/signer/session`, {
|
|
@@ -2079,18 +2187,94 @@ var TeeClient = class {
|
|
|
2079
2187
|
body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
|
|
2080
2188
|
});
|
|
2081
2189
|
if (!res.ok) throw new Error("session open failed");
|
|
2082
|
-
|
|
2190
|
+
const body = await res.json();
|
|
2191
|
+
this.handle = body.handle;
|
|
2192
|
+
return body.addresses ?? null;
|
|
2083
2193
|
}
|
|
2084
|
-
/**
|
|
2085
|
-
|
|
2194
|
+
/**
|
|
2195
|
+
* Flow B: exchange a recovery share (from a recovery-share file) for a
|
|
2196
|
+
* fresh device share. The server verifies the reconstructed master
|
|
2197
|
+
* actually matches this account's addresses before returning anything —
|
|
2198
|
+
* see the security note in the TEE's /signer/recover handler. Rotates the
|
|
2199
|
+
* whole share set: the caller MUST persist the returned recoveryShare as a
|
|
2200
|
+
* new recovery-share file, since the old one stops working immediately.
|
|
2201
|
+
*/
|
|
2202
|
+
async recoverWithShare(jwt, recoveryShare) {
|
|
2086
2203
|
await this.ensureAttested();
|
|
2087
|
-
const res = await fetch(`${this.teeUrl}/signer/
|
|
2204
|
+
const res = await fetch(`${this.teeUrl}/signer/recover`, {
|
|
2088
2205
|
method: "POST",
|
|
2089
2206
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2090
|
-
body: JSON.stringify({
|
|
2207
|
+
body: JSON.stringify({ recoveryShare: bytesToBase64(recoveryShare) })
|
|
2208
|
+
});
|
|
2209
|
+
if (res.status === 403) {
|
|
2210
|
+
const { RecoveryWalletMismatchError: RecoveryWalletMismatchError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
2211
|
+
throw new RecoveryWalletMismatchError2();
|
|
2212
|
+
}
|
|
2213
|
+
if (!res.ok) throw new Error("recovery failed");
|
|
2214
|
+
const body = await res.json();
|
|
2215
|
+
return {
|
|
2216
|
+
deviceShare: base64ToBytes(body.deviceShare),
|
|
2217
|
+
recoveryShare: base64ToBytes(body.recoveryShare),
|
|
2218
|
+
shareSetVersion: body.shareSetVersion
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Request a fresh WebAuthn assertion challenge and complete it in the
|
|
2223
|
+
* browser via @simplewebauthn/browser's startAuthentication(), which
|
|
2224
|
+
* handles all the base64url (de)serialization the raw navigator.credentials
|
|
2225
|
+
* API requires. Shared by rotateShares() and exportPortableBackup() — both
|
|
2226
|
+
* require proof of fresh authenticator possession, not just an active session.
|
|
2227
|
+
*/
|
|
2228
|
+
async completeAssertion(jwt) {
|
|
2229
|
+
const res = await fetch(`${this.teeUrl}/signer/assertion-challenge`, {
|
|
2230
|
+
method: "POST",
|
|
2231
|
+
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2232
|
+
body: JSON.stringify({ rpId: window.location.hostname })
|
|
2091
2233
|
});
|
|
2092
|
-
if (!res.ok)
|
|
2093
|
-
|
|
2234
|
+
if (!res.ok) throw new ExportNotAuthorizedError();
|
|
2235
|
+
const { challengeId, options } = await res.json();
|
|
2236
|
+
let assertion;
|
|
2237
|
+
try {
|
|
2238
|
+
assertion = await startAuthentication({ optionsJSON: options });
|
|
2239
|
+
} catch {
|
|
2240
|
+
const { UserCancelledError: UserCancelledError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
2241
|
+
throw new UserCancelledError2();
|
|
2242
|
+
}
|
|
2243
|
+
return { challengeId, assertion };
|
|
2244
|
+
}
|
|
2245
|
+
/** Voluntary rotation — e.g. after a suspected device compromise, or to invalidate an old recovery file. */
|
|
2246
|
+
async rotateShares(jwt) {
|
|
2247
|
+
if (!this.handle) throw new Error("no session");
|
|
2248
|
+
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2249
|
+
const res = await fetch(`${this.teeUrl}/signer/rotate`, {
|
|
2250
|
+
method: "POST",
|
|
2251
|
+
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2252
|
+
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
2253
|
+
});
|
|
2254
|
+
if (!res.ok) throw new ExportNotAuthorizedError();
|
|
2255
|
+
const body = await res.json();
|
|
2256
|
+
return {
|
|
2257
|
+
deviceShare: base64ToBytes(body.deviceShare),
|
|
2258
|
+
recoveryShare: base64ToBytes(body.recoveryShare),
|
|
2259
|
+
shareSetVersion: body.shareSetVersion
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
/** The two-share bearer portable backup — see recovery/portableBackup.ts. Rotates the set on export. */
|
|
2263
|
+
async exportPortableBackup(jwt) {
|
|
2264
|
+
if (!this.handle) throw new Error("no session");
|
|
2265
|
+
const { challengeId, assertion } = await this.completeAssertion(jwt);
|
|
2266
|
+
const res = await fetch(`${this.teeUrl}/signer/export-portable`, {
|
|
2267
|
+
method: "POST",
|
|
2268
|
+
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
|
|
2269
|
+
body: JSON.stringify({ handle: this.handle, challengeId, assertion, origin: window.location.origin })
|
|
2270
|
+
});
|
|
2271
|
+
if (!res.ok) throw new ExportNotAuthorizedError();
|
|
2272
|
+
const body = await res.json();
|
|
2273
|
+
return {
|
|
2274
|
+
deviceShare: base64ToBytes(body.deviceShare),
|
|
2275
|
+
recoveryShare: base64ToBytes(body.recoveryShare),
|
|
2276
|
+
shareSetVersion: body.shareSetVersion
|
|
2277
|
+
};
|
|
2094
2278
|
}
|
|
2095
2279
|
hasSession() {
|
|
2096
2280
|
return this.handle !== null;
|
|
@@ -2123,6 +2307,7 @@ var TeeClient = class {
|
|
|
2123
2307
|
};
|
|
2124
2308
|
|
|
2125
2309
|
// src/social/session.ts
|
|
2310
|
+
init_errors();
|
|
2126
2311
|
var SessionManager = class {
|
|
2127
2312
|
constructor(events, durationMs, teeUrl) {
|
|
2128
2313
|
this.events = events;
|
|
@@ -2137,9 +2322,24 @@ var SessionManager = class {
|
|
|
2137
2322
|
window.addEventListener("pagehide", () => this.end());
|
|
2138
2323
|
}
|
|
2139
2324
|
}
|
|
2140
|
-
|
|
2325
|
+
/**
|
|
2326
|
+
* Opens the TEE session and resolves which addresses this wallet really has.
|
|
2327
|
+
*
|
|
2328
|
+
* `fallback` is whatever the backend recorded; the TEE's answer is derived
|
|
2329
|
+
* from the signing key itself and therefore outranks it. Returns the
|
|
2330
|
+
* resolved addresses so callers can propagate the corrected values rather
|
|
2331
|
+
* than continuing to use a stale record.
|
|
2332
|
+
*/
|
|
2333
|
+
async start(deviceShare, jwt, fallback) {
|
|
2141
2334
|
this.tee = new TeeClient(this.teeUrl);
|
|
2142
|
-
await this.tee.openSession(deviceShare, jwt);
|
|
2335
|
+
const verified = await this.tee.openSession(deviceShare, jwt);
|
|
2336
|
+
const addresses = verified ?? fallback;
|
|
2337
|
+
if (!addresses) {
|
|
2338
|
+
this.tee = null;
|
|
2339
|
+
throw new Error(
|
|
2340
|
+
"No addresses available for this wallet \u2014 the signer could not derive them and none are recorded"
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2143
2343
|
this.evmAddress = addresses.evm;
|
|
2144
2344
|
this.solanaAddress = addresses.solana;
|
|
2145
2345
|
this.expiresAt = Date.now() + this.durationMs;
|
|
@@ -2149,6 +2349,7 @@ var SessionManager = class {
|
|
|
2149
2349
|
this.events.emit("session-expired");
|
|
2150
2350
|
}, this.durationMs);
|
|
2151
2351
|
this.events.emit("session-started");
|
|
2352
|
+
return addresses;
|
|
2152
2353
|
}
|
|
2153
2354
|
end() {
|
|
2154
2355
|
void this.tee?.closeSession();
|
|
@@ -2178,24 +2379,32 @@ var SessionManager = class {
|
|
|
2178
2379
|
};
|
|
2179
2380
|
|
|
2180
2381
|
// src/social/crypto/shares.ts
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
const out = new Uint8Array(a.length);
|
|
2184
|
-
for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i];
|
|
2185
|
-
return out;
|
|
2186
|
-
}
|
|
2382
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
2383
|
+
import { split, combine } from "shamir-secret-sharing";
|
|
2187
2384
|
function generateRandomBytes(n) {
|
|
2188
2385
|
return crypto.getRandomValues(new Uint8Array(n));
|
|
2189
2386
|
}
|
|
2190
|
-
function generateWalletShares() {
|
|
2191
|
-
const privateKey = generateRandomBytes(32);
|
|
2192
|
-
const deviceShare = generateRandomBytes(32);
|
|
2193
|
-
const serverShare = xorBytes(privateKey, deviceShare);
|
|
2194
|
-
return { privateKey, deviceShare, serverShare };
|
|
2195
|
-
}
|
|
2196
2387
|
function zeroize(bytes) {
|
|
2197
2388
|
bytes.fill(0);
|
|
2198
2389
|
}
|
|
2390
|
+
function generateMaster() {
|
|
2391
|
+
let master;
|
|
2392
|
+
do {
|
|
2393
|
+
master = generateRandomBytes(32);
|
|
2394
|
+
} while (!secp256k1.utils.isValidPrivateKey(master));
|
|
2395
|
+
return master;
|
|
2396
|
+
}
|
|
2397
|
+
async function splitMaster(master, version = 1) {
|
|
2398
|
+
if (master.length !== 32) throw new Error("master must be 32 bytes");
|
|
2399
|
+
const parts = await split(master, 3, 2);
|
|
2400
|
+
if (parts.length !== 3) throw new Error("unexpected share count");
|
|
2401
|
+
return { device: parts[0], server: parts[1], recovery: parts[2], version };
|
|
2402
|
+
}
|
|
2403
|
+
async function combineShares(a, b) {
|
|
2404
|
+
const master = await combine([a, b]);
|
|
2405
|
+
if (master.length !== 32) throw new Error("reconstruction produced wrong length");
|
|
2406
|
+
return master;
|
|
2407
|
+
}
|
|
2199
2408
|
|
|
2200
2409
|
// src/social/crypto/wrap.ts
|
|
2201
2410
|
function buf(u) {
|
|
@@ -2240,6 +2449,8 @@ function validatePin(pin) {
|
|
|
2240
2449
|
}
|
|
2241
2450
|
|
|
2242
2451
|
// src/social/crypto/passkey.ts
|
|
2452
|
+
init_errors();
|
|
2453
|
+
import { startRegistration } from "@simplewebauthn/browser";
|
|
2243
2454
|
function buf3(u) {
|
|
2244
2455
|
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2245
2456
|
}
|
|
@@ -2247,30 +2458,24 @@ function isPasskeySupported() {
|
|
|
2247
2458
|
return typeof window !== "undefined" && !!window.PublicKeyCredential && "isUserVerifyingPlatformAuthenticatorAvailable" in window.PublicKeyCredential;
|
|
2248
2459
|
}
|
|
2249
2460
|
async function registerPasskey(opts) {
|
|
2250
|
-
|
|
2251
|
-
let cred;
|
|
2461
|
+
let response;
|
|
2252
2462
|
try {
|
|
2253
|
-
|
|
2254
|
-
publicKey: {
|
|
2255
|
-
challenge: buf3(challenge),
|
|
2256
|
-
rp: { id: opts.rpId, name: opts.rpName },
|
|
2257
|
-
user: { id: buf3(opts.userId), name: opts.userName, displayName: opts.userName },
|
|
2258
|
-
pubKeyCredParams: [
|
|
2259
|
-
{ type: "public-key", alg: -7 },
|
|
2260
|
-
{ type: "public-key", alg: -257 }
|
|
2261
|
-
],
|
|
2262
|
-
authenticatorSelection: { userVerification: "required", residentKey: "preferred" },
|
|
2263
|
-
extensions: { prf: { eval: { first: buf3(opts.prfSalt) } } }
|
|
2264
|
-
}
|
|
2265
|
-
});
|
|
2463
|
+
response = await startRegistration({ optionsJSON: opts.optionsJSON });
|
|
2266
2464
|
} catch (e) {
|
|
2267
2465
|
if (e.name === "NotAllowedError") throw new UserCancelledError();
|
|
2268
2466
|
throw e;
|
|
2269
2467
|
}
|
|
2270
|
-
const
|
|
2271
|
-
const prfResults =
|
|
2272
|
-
|
|
2273
|
-
|
|
2468
|
+
const extResults = response.clientExtensionResults;
|
|
2469
|
+
const prfResults = extResults?.prf?.results;
|
|
2470
|
+
let prfOutput;
|
|
2471
|
+
if (prfResults?.first) {
|
|
2472
|
+
prfOutput = typeof prfResults.first === "string" ? Uint8Array.from(atob(prfResults.first.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)) : new Uint8Array(prfResults.first);
|
|
2473
|
+
}
|
|
2474
|
+
const credentialId = Uint8Array.from(
|
|
2475
|
+
atob(response.id.replace(/-/g, "+").replace(/_/g, "/")),
|
|
2476
|
+
(c) => c.charCodeAt(0)
|
|
2477
|
+
);
|
|
2478
|
+
return { credentialId, prfOutput, response };
|
|
2274
2479
|
}
|
|
2275
2480
|
async function getPrfOutput(opts) {
|
|
2276
2481
|
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
@@ -2295,6 +2500,28 @@ async function getPrfOutput(opts) {
|
|
|
2295
2500
|
return new Uint8Array(prfResults.first);
|
|
2296
2501
|
}
|
|
2297
2502
|
|
|
2503
|
+
// src/social/webauthnRegistration.ts
|
|
2504
|
+
function bufFromBytes(u) {
|
|
2505
|
+
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2506
|
+
}
|
|
2507
|
+
async function registerWebAuthnCredential(opts) {
|
|
2508
|
+
const baseOptions = await opts.api.getWebAuthnRegisterOptions(opts.jwt, opts.rpId, opts.rpName);
|
|
2509
|
+
const optionsJSON = {
|
|
2510
|
+
...baseOptions,
|
|
2511
|
+
extensions: {
|
|
2512
|
+
...baseOptions.extensions,
|
|
2513
|
+
prf: { eval: { first: bufFromBytes(opts.prfSalt) } }
|
|
2514
|
+
}
|
|
2515
|
+
};
|
|
2516
|
+
const { credentialId, prfOutput, response } = await registerPasskey({ optionsJSON });
|
|
2517
|
+
try {
|
|
2518
|
+
await opts.api.postWebAuthnRegister(opts.jwt, response, window.location.origin);
|
|
2519
|
+
} catch (e) {
|
|
2520
|
+
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);
|
|
2521
|
+
}
|
|
2522
|
+
return { credentialId, prfOutput };
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2298
2525
|
// src/social/auth/email.ts
|
|
2299
2526
|
async function startEmailAuth(api, email) {
|
|
2300
2527
|
await api.authEmailStart(email);
|
|
@@ -2341,28 +2568,102 @@ async function fetchWrappedShareBackup(api, jwt) {
|
|
|
2341
2568
|
return api.getRecoveryBackup(jwt);
|
|
2342
2569
|
}
|
|
2343
2570
|
|
|
2344
|
-
// src/social/recovery/
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2571
|
+
// src/social/recovery/recoveryShareFile.ts
|
|
2572
|
+
init_errors();
|
|
2573
|
+
var PBKDF2_ITERATIONS2 = 6e5;
|
|
2574
|
+
var FILE_VERSION = 2;
|
|
2575
|
+
function b642(u) {
|
|
2576
|
+
let s = "";
|
|
2577
|
+
for (const b of u) s += String.fromCharCode(b);
|
|
2578
|
+
return btoa(s);
|
|
2350
2579
|
}
|
|
2351
|
-
function
|
|
2352
|
-
|
|
2353
|
-
if (!validateMnemonic(normalized, wordlist)) throw new InvalidPhraseError();
|
|
2354
|
-
const entropy = mnemonicToEntropy(normalized, wordlist);
|
|
2355
|
-
if (entropy.length !== 32) throw new InvalidPhraseError();
|
|
2356
|
-
return entropy;
|
|
2580
|
+
function unb642(s) {
|
|
2581
|
+
return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
|
2357
2582
|
}
|
|
2358
|
-
function
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2583
|
+
function buf4(u) {
|
|
2584
|
+
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2585
|
+
}
|
|
2586
|
+
async function deriveFileKey(password, salt, iterations) {
|
|
2587
|
+
const base = await crypto.subtle.importKey(
|
|
2588
|
+
"raw",
|
|
2589
|
+
new TextEncoder().encode(password),
|
|
2590
|
+
"PBKDF2",
|
|
2591
|
+
false,
|
|
2592
|
+
["deriveKey"]
|
|
2593
|
+
);
|
|
2594
|
+
return crypto.subtle.deriveKey(
|
|
2595
|
+
{ name: "PBKDF2", salt: buf4(salt), iterations, hash: "SHA-256" },
|
|
2596
|
+
base,
|
|
2597
|
+
{ name: "AES-GCM", length: 256 },
|
|
2598
|
+
false,
|
|
2599
|
+
["encrypt", "decrypt"]
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
async function buildRecoveryShareFile(share, password, addresses, shareSetVersion, passwordHint) {
|
|
2603
|
+
if (share.length !== 33) throw new Error("share must be 33 bytes");
|
|
2604
|
+
if (password.length < 8) throw new Error("password must be at least 8 characters");
|
|
2605
|
+
if (passwordHint && passwordHint.trim() === password) {
|
|
2606
|
+
throw new Error("password hint must not be the password");
|
|
2607
|
+
}
|
|
2608
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
2609
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
2610
|
+
const key = await deriveFileKey(password, salt, PBKDF2_ITERATIONS2);
|
|
2611
|
+
const ct = new Uint8Array(
|
|
2612
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(share))
|
|
2613
|
+
);
|
|
2614
|
+
return {
|
|
2615
|
+
version: FILE_VERSION,
|
|
2616
|
+
type: "decane-recovery-share",
|
|
2617
|
+
shareSetVersion,
|
|
2618
|
+
kdf: "pbkdf2-sha256",
|
|
2619
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS2, salt: b642(salt) },
|
|
2620
|
+
cipher: "aes-256-gcm",
|
|
2621
|
+
iv: b642(iv),
|
|
2622
|
+
ciphertext: b642(ct),
|
|
2623
|
+
addresses,
|
|
2624
|
+
passwordHint: passwordHint?.trim() || null,
|
|
2625
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function parseRecoveryShareFile(json) {
|
|
2629
|
+
const raw = json;
|
|
2630
|
+
if (raw?.type === "decane-recovery") {
|
|
2631
|
+
throw new InvalidRecoveryFileError();
|
|
2632
|
+
}
|
|
2633
|
+
const f = json;
|
|
2634
|
+
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") {
|
|
2635
|
+
throw new InvalidRecoveryFileError();
|
|
2636
|
+
}
|
|
2637
|
+
if (f.kdfParams.iterations < 1e5 || f.kdfParams.iterations > 5e6) {
|
|
2638
|
+
throw new InvalidRecoveryFileError();
|
|
2639
|
+
}
|
|
2640
|
+
return f;
|
|
2641
|
+
}
|
|
2642
|
+
async function decryptRecoveryShareFile(file, password) {
|
|
2643
|
+
const salt = unb642(file.kdfParams.salt);
|
|
2644
|
+
const iv = unb642(file.iv);
|
|
2645
|
+
const ct = unb642(file.ciphertext);
|
|
2646
|
+
const key = await deriveFileKey(password, salt, file.kdfParams.iterations);
|
|
2647
|
+
let share;
|
|
2648
|
+
try {
|
|
2649
|
+
share = new Uint8Array(
|
|
2650
|
+
await crypto.subtle.decrypt({ name: "AES-GCM", iv: buf4(iv) }, key, buf4(ct))
|
|
2651
|
+
);
|
|
2652
|
+
} catch {
|
|
2653
|
+
throw new WrongPasswordError();
|
|
2654
|
+
}
|
|
2655
|
+
if (share.length !== 33) {
|
|
2656
|
+
share.fill(0);
|
|
2657
|
+
throw new InvalidRecoveryFileError();
|
|
2658
|
+
}
|
|
2659
|
+
return share;
|
|
2660
|
+
}
|
|
2661
|
+
function recoveryShareFileName(evmAddress) {
|
|
2662
|
+
return `decane-recovery-${evmAddress.slice(0, 8)}.json`;
|
|
2363
2663
|
}
|
|
2364
2664
|
|
|
2365
2665
|
// src/social/recovery/recoveryFlow.ts
|
|
2666
|
+
init_errors();
|
|
2366
2667
|
async function recoverDeviceShare(input) {
|
|
2367
2668
|
const backup = await fetchWrappedShareBackup(input.api, input.jwt);
|
|
2368
2669
|
if (backup) {
|
|
@@ -2396,37 +2697,87 @@ async function recoverDeviceShare(input) {
|
|
|
2396
2697
|
});
|
|
2397
2698
|
}
|
|
2398
2699
|
}
|
|
2399
|
-
if (!input.
|
|
2400
|
-
input.onProgress?.({ type: "
|
|
2401
|
-
const
|
|
2402
|
-
if (!
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
}
|
|
2409
|
-
input.onProgress?.({ type: "phrase-verifying" });
|
|
2700
|
+
if (!input.promptForRecoveryFile) throw new NewDeviceError();
|
|
2701
|
+
input.onProgress?.({ type: "recovery-file-prompt" });
|
|
2702
|
+
const supplied = await input.promptForRecoveryFile();
|
|
2703
|
+
if (!supplied) throw new NewDeviceError();
|
|
2704
|
+
const file = parseRecoveryShareFile(supplied.value);
|
|
2705
|
+
input.onProgress?.({ type: "password-prompt", passwordHint: file.passwordHint });
|
|
2706
|
+
const password = await supplied.getPassword();
|
|
2707
|
+
if (!password) throw new NewDeviceError();
|
|
2708
|
+
const recoveryShare = await decryptRecoveryShareFile(file, password);
|
|
2709
|
+
input.onProgress?.({ type: "recovery-verifying" });
|
|
2410
2710
|
const tee = new TeeClient(input.teeUrl);
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2711
|
+
let result;
|
|
2712
|
+
try {
|
|
2713
|
+
result = await tee.recoverWithShare(input.jwt, recoveryShare);
|
|
2714
|
+
} finally {
|
|
2715
|
+
zeroize(recoveryShare);
|
|
2415
2716
|
}
|
|
2416
|
-
|
|
2717
|
+
input.onProgress?.({ type: "rotated", shareSetVersion: result.shareSetVersion });
|
|
2718
|
+
await input.onNewRecoveryShare?.(result.recoveryShare, result.shareSetVersion);
|
|
2719
|
+
zeroize(result.recoveryShare);
|
|
2720
|
+
return result.deviceShare;
|
|
2417
2721
|
}
|
|
2418
2722
|
|
|
2419
|
-
// src/social/recovery/
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2723
|
+
// src/social/recovery/portableBackup.ts
|
|
2724
|
+
init_errors();
|
|
2725
|
+
var PBKDF2_ITERATIONS3 = 6e5;
|
|
2726
|
+
var FILE_VERSION2 = 1;
|
|
2727
|
+
function b643(u) {
|
|
2728
|
+
let s = "";
|
|
2729
|
+
for (const b of u) s += String.fromCharCode(b);
|
|
2730
|
+
return btoa(s);
|
|
2423
2731
|
}
|
|
2424
|
-
function
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2732
|
+
function buf5(u) {
|
|
2733
|
+
return u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
|
|
2734
|
+
}
|
|
2735
|
+
async function deriveFileKey2(password, salt, iterations) {
|
|
2736
|
+
const base = await crypto.subtle.importKey(
|
|
2737
|
+
"raw",
|
|
2738
|
+
new TextEncoder().encode(password),
|
|
2739
|
+
"PBKDF2",
|
|
2740
|
+
false,
|
|
2741
|
+
["deriveKey"]
|
|
2742
|
+
);
|
|
2743
|
+
return crypto.subtle.deriveKey(
|
|
2744
|
+
{ name: "PBKDF2", salt: buf5(salt), iterations, hash: "SHA-256" },
|
|
2745
|
+
base,
|
|
2746
|
+
{ name: "AES-GCM", length: 256 },
|
|
2747
|
+
false,
|
|
2748
|
+
["encrypt", "decrypt"]
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
async function buildPortableBackup(deviceShare, recoveryShare, password, addresses, shareSetVersion, passwordHint) {
|
|
2752
|
+
if (deviceShare.length !== 33 || recoveryShare.length !== 33) {
|
|
2753
|
+
throw new Error("shares must be 33 bytes");
|
|
2754
|
+
}
|
|
2755
|
+
if (password.length < 8) throw new Error("password must be at least 8 characters");
|
|
2756
|
+
if (passwordHint && passwordHint.trim() === password) {
|
|
2757
|
+
throw new Error("password hint must not be the password");
|
|
2758
|
+
}
|
|
2759
|
+
const plaintext = new TextEncoder().encode(
|
|
2760
|
+
JSON.stringify({ deviceShare: b643(deviceShare), recoveryShare: b643(recoveryShare) })
|
|
2761
|
+
);
|
|
2762
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
2763
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
2764
|
+
const key = await deriveFileKey2(password, salt, PBKDF2_ITERATIONS3);
|
|
2765
|
+
const ct = new Uint8Array(
|
|
2766
|
+
await crypto.subtle.encrypt({ name: "AES-GCM", iv: buf5(iv) }, key, buf5(plaintext))
|
|
2767
|
+
);
|
|
2768
|
+
return {
|
|
2769
|
+
version: FILE_VERSION2,
|
|
2770
|
+
type: "decane-portable-backup",
|
|
2771
|
+
shareSetVersion,
|
|
2772
|
+
kdf: "pbkdf2-sha256",
|
|
2773
|
+
kdfParams: { iterations: PBKDF2_ITERATIONS3, salt: b643(salt) },
|
|
2774
|
+
cipher: "aes-256-gcm",
|
|
2775
|
+
iv: b643(iv),
|
|
2776
|
+
ciphertext: b643(ct),
|
|
2777
|
+
addresses,
|
|
2778
|
+
passwordHint: passwordHint?.trim() || null,
|
|
2779
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2780
|
+
};
|
|
2430
2781
|
}
|
|
2431
2782
|
|
|
2432
2783
|
// src/social/client.ts
|
|
@@ -2498,6 +2849,18 @@ async function createDecaneConnect(config) {
|
|
|
2498
2849
|
}
|
|
2499
2850
|
};
|
|
2500
2851
|
}
|
|
2852
|
+
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
2853
|
+
const stored = await storage.getShare(userId);
|
|
2854
|
+
if (!stored) {
|
|
2855
|
+
throw new CallSequenceError(
|
|
2856
|
+
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
const wrappingKey = await getWrappingKey(stored);
|
|
2860
|
+
const iv = generateRandomBytes(12);
|
|
2861
|
+
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
2862
|
+
await storage.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
2863
|
+
}
|
|
2501
2864
|
async function getWrappingKey(stored) {
|
|
2502
2865
|
if (stored.unlockMethod === "passkey") {
|
|
2503
2866
|
const prfOutput = await getPrfOutput({
|
|
@@ -2517,15 +2880,7 @@ async function createDecaneConnect(config) {
|
|
|
2517
2880
|
let credentialId;
|
|
2518
2881
|
let prfOutput;
|
|
2519
2882
|
try {
|
|
2520
|
-
const
|
|
2521
|
-
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2522
|
-
const result = await registerPasskey({
|
|
2523
|
-
rpId,
|
|
2524
|
-
rpName,
|
|
2525
|
-
userId: new TextEncoder().encode(userId),
|
|
2526
|
-
userName: `decane-wallet-${authType}-${dateStr}`,
|
|
2527
|
-
prfSalt
|
|
2528
|
-
});
|
|
2883
|
+
const result = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
|
|
2529
2884
|
credentialId = result.credentialId;
|
|
2530
2885
|
prfOutput = result.prfOutput;
|
|
2531
2886
|
} catch (e) {
|
|
@@ -2564,15 +2919,7 @@ async function createDecaneConnect(config) {
|
|
|
2564
2919
|
if (isPasskeySupported()) {
|
|
2565
2920
|
const prfSalt = generateRandomBytes(32);
|
|
2566
2921
|
try {
|
|
2567
|
-
const
|
|
2568
|
-
const dateStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2569
|
-
const { credentialId, prfOutput } = await registerPasskey({
|
|
2570
|
-
rpId,
|
|
2571
|
-
rpName,
|
|
2572
|
-
userId: new TextEncoder().encode(userId),
|
|
2573
|
-
userName: `decane-wallet-${authType}-${dateStr}`,
|
|
2574
|
-
prfSalt
|
|
2575
|
-
});
|
|
2922
|
+
const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
|
|
2576
2923
|
if (prfOutput) {
|
|
2577
2924
|
unlockMethod = "passkey";
|
|
2578
2925
|
wrappingKey = await importAesKey(prfOutput);
|
|
@@ -2595,19 +2942,21 @@ async function createDecaneConnect(config) {
|
|
|
2595
2942
|
({ wrappingKey, meta } = await pinSetup());
|
|
2596
2943
|
unlockMethod = "pin";
|
|
2597
2944
|
}
|
|
2598
|
-
const
|
|
2945
|
+
const master = generateMaster();
|
|
2946
|
+
let shareSet;
|
|
2599
2947
|
let evmAddress;
|
|
2600
2948
|
let solanaAddress;
|
|
2601
2949
|
try {
|
|
2602
|
-
evmAddress = new Wallet(toHex(
|
|
2603
|
-
solanaAddress = Keypair.fromSeed(
|
|
2950
|
+
evmAddress = new Wallet(toHex(master)).address;
|
|
2951
|
+
solanaAddress = Keypair.fromSeed(master).publicKey.toBase58();
|
|
2952
|
+
shareSet = await splitMaster(master, 1);
|
|
2604
2953
|
} finally {
|
|
2605
|
-
zeroize(
|
|
2954
|
+
zeroize(master);
|
|
2606
2955
|
}
|
|
2607
2956
|
const iv = generateRandomBytes(12);
|
|
2608
2957
|
const wrappedDeviceShare = await aesGcmEncrypt(
|
|
2609
2958
|
wrappingKey,
|
|
2610
|
-
|
|
2959
|
+
shareSet.device,
|
|
2611
2960
|
iv
|
|
2612
2961
|
);
|
|
2613
2962
|
await storage.putShare({
|
|
@@ -2623,7 +2972,7 @@ async function createDecaneConnect(config) {
|
|
|
2623
2972
|
});
|
|
2624
2973
|
const serverIv = generateRandomBytes(12);
|
|
2625
2974
|
await api.putShare(jwt, {
|
|
2626
|
-
ciphertext:
|
|
2975
|
+
ciphertext: shareSet.server,
|
|
2627
2976
|
iv: serverIv,
|
|
2628
2977
|
wrappedShareMeta: {
|
|
2629
2978
|
unlockMethod
|
|
@@ -2632,7 +2981,7 @@ async function createDecaneConnect(config) {
|
|
|
2632
2981
|
evmAddress,
|
|
2633
2982
|
solanaAddress
|
|
2634
2983
|
});
|
|
2635
|
-
zeroize(
|
|
2984
|
+
zeroize(shareSet.server);
|
|
2636
2985
|
const addresses = { evm: evmAddress, solana: solanaAddress };
|
|
2637
2986
|
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
|
|
2638
2987
|
try {
|
|
@@ -2647,32 +2996,47 @@ async function createDecaneConnect(config) {
|
|
|
2647
2996
|
console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
|
|
2648
2997
|
}
|
|
2649
2998
|
}
|
|
2650
|
-
await session.start(
|
|
2651
|
-
|
|
2999
|
+
const confirmed = await session.start(shareSet.device, jwt, addresses);
|
|
3000
|
+
zeroize(shareSet.device);
|
|
3001
|
+
if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3002
|
+
console.warn(
|
|
3003
|
+
`decane-connect-kit: signer derived a different EVM address (${confirmed.evm}) than this device did (${addresses.evm}) during setup. Using the signer's.`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
addresses.evm = evmAddress = confirmed.evm;
|
|
3007
|
+
addresses.solana = solanaAddress = confirmed.solana;
|
|
3008
|
+
if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
|
|
2652
3009
|
try {
|
|
2653
|
-
const
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
3010
|
+
const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
|
|
3011
|
+
if (choice.wants) {
|
|
3012
|
+
const file = await buildRecoveryShareFile(
|
|
3013
|
+
shareSet.recovery,
|
|
3014
|
+
choice.password,
|
|
3015
|
+
addresses,
|
|
3016
|
+
shareSet.version,
|
|
3017
|
+
choice.passwordHint
|
|
3018
|
+
);
|
|
3019
|
+
await config.onRecoveryFileReady?.(file, recoveryShareFileName(evmAddress));
|
|
3020
|
+
await api.markRecoveryShareSaved(jwt);
|
|
2657
3021
|
}
|
|
2658
3022
|
} catch (e) {
|
|
2659
|
-
console.warn("decane-connect-kit: recovery
|
|
3023
|
+
console.warn("decane-connect-kit: recovery share offer failed or was skipped.", e);
|
|
2660
3024
|
}
|
|
2661
3025
|
}
|
|
2662
|
-
zeroize(
|
|
3026
|
+
zeroize(shareSet.recovery);
|
|
2663
3027
|
return addresses;
|
|
2664
3028
|
}
|
|
2665
3029
|
async function returningUserUnlock(userId, jwt) {
|
|
2666
3030
|
const rawAddresses = await api.getAddresses(jwt);
|
|
2667
|
-
|
|
2668
|
-
throw new CallSequenceError(
|
|
2669
|
-
"No addresses are recorded for this user \u2014 inconsistent server state"
|
|
2670
|
-
);
|
|
2671
|
-
}
|
|
2672
|
-
const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
|
|
3031
|
+
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
|
|
2673
3032
|
const stored = await storage.getShare(userId);
|
|
2674
3033
|
let deviceShare;
|
|
2675
3034
|
if (!stored) {
|
|
3035
|
+
if (!config.onRecoveryRotated) {
|
|
3036
|
+
throw new CallSequenceError(
|
|
3037
|
+
"Recovery rotates the share set and issues a new recovery file, but no onRecoveryRotated callback is configured to save it. Configure onRecoveryRotated before allowing recovery."
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
2676
3040
|
deviceShare = await recoverDeviceShare({
|
|
2677
3041
|
userId,
|
|
2678
3042
|
jwt,
|
|
@@ -2681,7 +3045,15 @@ async function createDecaneConnect(config) {
|
|
|
2681
3045
|
api,
|
|
2682
3046
|
storage,
|
|
2683
3047
|
onProgress: config.onRecoveryProgress,
|
|
2684
|
-
|
|
3048
|
+
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
3049
|
+
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
3050
|
+
if (!config.onRecoveryRotated) return;
|
|
3051
|
+
if (!recordedAddresses) return;
|
|
3052
|
+
const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: recordedAddresses.evm, solanaAddress: recordedAddresses.solana });
|
|
3053
|
+
const file = await buildRecoveryShareFile(recoveryShare, password, recordedAddresses, shareSetVersion, passwordHint);
|
|
3054
|
+
await config.onRecoveryFileReady?.(file, recoveryShareFileName(recordedAddresses.evm));
|
|
3055
|
+
await api.markRecoveryShareSaved(jwt);
|
|
3056
|
+
}
|
|
2685
3057
|
});
|
|
2686
3058
|
if (!await storage.getShare(userId) && isPasskeySupported()) {
|
|
2687
3059
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
@@ -2698,8 +3070,13 @@ async function createDecaneConnect(config) {
|
|
|
2698
3070
|
throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
|
|
2699
3071
|
}
|
|
2700
3072
|
}
|
|
2701
|
-
await session.start(deviceShare, jwt,
|
|
3073
|
+
const addresses = await session.start(deviceShare, jwt, recordedAddresses);
|
|
2702
3074
|
zeroize(deviceShare);
|
|
3075
|
+
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3076
|
+
console.warn(
|
|
3077
|
+
`decane-connect-kit: recorded EVM address (${recordedAddresses.evm}) does not match the signing key (${addresses.evm}). Using the signing key's address; the record has been corrected.`
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
2703
3080
|
return addresses;
|
|
2704
3081
|
}
|
|
2705
3082
|
async function handleAuthResult(result) {
|
|
@@ -2761,14 +3138,24 @@ async function createDecaneConnect(config) {
|
|
|
2761
3138
|
return handleAuthResult(result);
|
|
2762
3139
|
},
|
|
2763
3140
|
async unlock() {
|
|
2764
|
-
if (!auth)
|
|
2765
|
-
throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3141
|
+
if (!auth) {
|
|
3142
|
+
throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3143
|
+
}
|
|
2766
3144
|
if (session.isActive()) return;
|
|
2767
3145
|
await returningUserUnlock(auth.userId, auth.jwt);
|
|
2768
3146
|
},
|
|
2769
3147
|
isUnlocked() {
|
|
2770
3148
|
return session.isActive();
|
|
2771
3149
|
},
|
|
3150
|
+
/**
|
|
3151
|
+
* True when we have a persisted identity but no live auth state, i.e.
|
|
3152
|
+
* unlock() cannot succeed and the user must sign in again. Exposed on the
|
|
3153
|
+
* client (not just the React context) so non-React consumers can branch on
|
|
3154
|
+
* it without having to catch ReconnectRequiredError.
|
|
3155
|
+
*/
|
|
3156
|
+
needsReconnect() {
|
|
3157
|
+
return !auth && !!persisted;
|
|
3158
|
+
},
|
|
2772
3159
|
sessionExpiresAt() {
|
|
2773
3160
|
return session.expiresAtTimestamp();
|
|
2774
3161
|
},
|
|
@@ -2856,23 +3243,86 @@ async function createDecaneConnect(config) {
|
|
|
2856
3243
|
getAddresses() {
|
|
2857
3244
|
return auth?.addresses ?? persisted?.addresses ?? null;
|
|
2858
3245
|
},
|
|
2859
|
-
async
|
|
3246
|
+
async rotateShares(opts) {
|
|
2860
3247
|
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
2861
|
-
const
|
|
2862
|
-
|
|
2863
|
-
const wrappingKey = await getWrappingKey(stored);
|
|
2864
|
-
const deviceShare = await aesGcmDecrypt(wrappingKey, stored.wrappedDeviceShare, stored.iv);
|
|
3248
|
+
const tee = session.requireActive();
|
|
3249
|
+
const result = await tee.rotateShares(auth.jwt);
|
|
2865
3250
|
try {
|
|
2866
|
-
|
|
3251
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3252
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3253
|
+
const file = await buildRecoveryShareFile(
|
|
3254
|
+
result.recoveryShare,
|
|
3255
|
+
opts.password,
|
|
3256
|
+
addresses,
|
|
3257
|
+
result.shareSetVersion,
|
|
3258
|
+
opts.passwordHint
|
|
3259
|
+
);
|
|
3260
|
+
await api.markRecoveryShareSaved(auth.jwt);
|
|
3261
|
+
return file;
|
|
2867
3262
|
} finally {
|
|
2868
|
-
zeroize(deviceShare);
|
|
3263
|
+
zeroize(result.deviceShare);
|
|
3264
|
+
zeroize(result.recoveryShare);
|
|
2869
3265
|
}
|
|
2870
3266
|
},
|
|
2871
|
-
async
|
|
3267
|
+
async exportPortableBackup(opts) {
|
|
2872
3268
|
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
2873
|
-
|
|
2874
|
-
await
|
|
2875
|
-
|
|
3269
|
+
const tee = session.requireActive();
|
|
3270
|
+
const result = await tee.exportPortableBackup(auth.jwt);
|
|
3271
|
+
try {
|
|
3272
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3273
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3274
|
+
return await buildPortableBackup(
|
|
3275
|
+
result.deviceShare,
|
|
3276
|
+
result.recoveryShare,
|
|
3277
|
+
opts.password,
|
|
3278
|
+
addresses,
|
|
3279
|
+
result.shareSetVersion,
|
|
3280
|
+
opts.passwordHint
|
|
3281
|
+
);
|
|
3282
|
+
} finally {
|
|
3283
|
+
zeroize(result.deviceShare);
|
|
3284
|
+
zeroize(result.recoveryShare);
|
|
3285
|
+
}
|
|
3286
|
+
},
|
|
3287
|
+
async hasRecoveryShare() {
|
|
3288
|
+
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3289
|
+
const status = await api.getRecoveryStatus(auth.jwt);
|
|
3290
|
+
return status.hasRecoveryShare;
|
|
3291
|
+
},
|
|
3292
|
+
async shouldPromptRecovery() {
|
|
3293
|
+
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3294
|
+
const status = await api.getRecoveryStatus(auth.jwt);
|
|
3295
|
+
if (status.hasRecoveryShare || status.nagged) return false;
|
|
3296
|
+
try {
|
|
3297
|
+
const checks = [];
|
|
3298
|
+
if (auth.addresses.evm) {
|
|
3299
|
+
const { JsonRpcProvider } = await import("ethers");
|
|
3300
|
+
for (const chain of config.chains.filter((c) => c.startsWith("evm:"))) {
|
|
3301
|
+
const rpcUrl = config.rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
|
|
3302
|
+
if (!rpcUrl) continue;
|
|
3303
|
+
checks.push(
|
|
3304
|
+
new JsonRpcProvider(rpcUrl).getBalance(auth.addresses.evm).then((b) => b > 0n).catch(() => false)
|
|
3305
|
+
);
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
if (auth.addresses.solana && config.chains.some((c) => c.startsWith("solana:"))) {
|
|
3309
|
+
const { Connection, PublicKey } = await import("@solana/web3.js");
|
|
3310
|
+
const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
3311
|
+
const cluster = solChain.slice("solana:".length);
|
|
3312
|
+
const endpoint = cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
|
|
3313
|
+
checks.push(
|
|
3314
|
+
new Connection(endpoint).getBalance(new PublicKey(auth.addresses.solana)).then((b) => b > 0).catch(() => false)
|
|
3315
|
+
);
|
|
3316
|
+
}
|
|
3317
|
+
const results = await Promise.all(checks);
|
|
3318
|
+
return results.some(Boolean);
|
|
3319
|
+
} catch {
|
|
3320
|
+
return false;
|
|
3321
|
+
}
|
|
3322
|
+
},
|
|
3323
|
+
async dismissRecoveryPrompt() {
|
|
3324
|
+
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3325
|
+
await api.dismissRecoveryNag(auth.jwt);
|
|
2876
3326
|
},
|
|
2877
3327
|
async hasBackup() {
|
|
2878
3328
|
if (!auth) throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
@@ -2887,16 +3337,30 @@ async function createDecaneConnect(config) {
|
|
|
2887
3337
|
};
|
|
2888
3338
|
return client;
|
|
2889
3339
|
}
|
|
2890
|
-
function bufferToB64u(
|
|
2891
|
-
return btoa(String.fromCharCode(...
|
|
3340
|
+
function bufferToB64u(buf6) {
|
|
3341
|
+
return btoa(String.fromCharCode(...buf6)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
2892
3342
|
}
|
|
2893
3343
|
function b64uToBuffer(b64u) {
|
|
2894
|
-
const
|
|
2895
|
-
return Uint8Array.from(atob(
|
|
3344
|
+
const b644 = b64u.replace(/-/g, "+").replace(/_/g, "/");
|
|
3345
|
+
return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
|
|
2896
3346
|
}
|
|
2897
3347
|
function toHex(bytes) {
|
|
2898
3348
|
return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2899
3349
|
}
|
|
3350
|
+
async function deriveAddressesFromShares(a, b) {
|
|
3351
|
+
const master = await combineShares(a, b);
|
|
3352
|
+
try {
|
|
3353
|
+
return {
|
|
3354
|
+
evm: new Wallet(toHex(master)).address,
|
|
3355
|
+
solana: Keypair.fromSeed(master).publicKey.toBase58()
|
|
3356
|
+
};
|
|
3357
|
+
} finally {
|
|
3358
|
+
zeroize(master);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/social/index.ts
|
|
3363
|
+
init_errors();
|
|
2900
3364
|
|
|
2901
3365
|
// src/ui/WalletSelector.tsx
|
|
2902
3366
|
import React2, { useEffect, useCallback, useState } from "react";
|
|
@@ -3819,6 +4283,15 @@ function countdown(expiresAt) {
|
|
|
3819
4283
|
const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
3820
4284
|
return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
3821
4285
|
}
|
|
4286
|
+
function downloadJson(data, filename) {
|
|
4287
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
4288
|
+
const url = URL.createObjectURL(blob);
|
|
4289
|
+
const a = document.createElement("a");
|
|
4290
|
+
a.href = url;
|
|
4291
|
+
a.download = filename;
|
|
4292
|
+
a.click();
|
|
4293
|
+
URL.revokeObjectURL(url);
|
|
4294
|
+
}
|
|
3822
4295
|
function CopyButton({ value }) {
|
|
3823
4296
|
const [copied, setCopied] = useState2(false);
|
|
3824
4297
|
async function copy() {
|
|
@@ -3846,7 +4319,54 @@ function CopyButton({ value }) {
|
|
|
3846
4319
|
copied ? "\u2713" : "copy"
|
|
3847
4320
|
);
|
|
3848
4321
|
}
|
|
3849
|
-
function
|
|
4322
|
+
function useBalance(kind, address, chain, rpcUrls) {
|
|
4323
|
+
const [state, setState] = useState2({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4324
|
+
useEffect2(() => {
|
|
4325
|
+
if (!address || !chain) return;
|
|
4326
|
+
let cancelled = false;
|
|
4327
|
+
setState((s) => ({ ...s, loading: true }));
|
|
4328
|
+
(async () => {
|
|
4329
|
+
try {
|
|
4330
|
+
if (kind === "evm") {
|
|
4331
|
+
const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
|
|
4332
|
+
if (!rpcUrl) {
|
|
4333
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
const { JsonRpcProvider, formatEther } = await import("ethers");
|
|
4337
|
+
const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
|
|
4338
|
+
if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
|
|
4339
|
+
} else {
|
|
4340
|
+
const cluster = chain.slice("solana:".length);
|
|
4341
|
+
const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
|
|
4342
|
+
const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
|
|
4343
|
+
const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
|
|
4344
|
+
if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
|
|
4345
|
+
}
|
|
4346
|
+
} catch {
|
|
4347
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4348
|
+
}
|
|
4349
|
+
})();
|
|
4350
|
+
return () => {
|
|
4351
|
+
cancelled = true;
|
|
4352
|
+
};
|
|
4353
|
+
}, [kind, address, chain, rpcUrls]);
|
|
4354
|
+
return state;
|
|
4355
|
+
}
|
|
4356
|
+
function formatBalance(value) {
|
|
4357
|
+
if (value == null) return "\u2014";
|
|
4358
|
+
const n = Number(value);
|
|
4359
|
+
if (!Number.isFinite(n)) return value;
|
|
4360
|
+
return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
|
|
4361
|
+
}
|
|
4362
|
+
function AddressRow({
|
|
4363
|
+
label,
|
|
4364
|
+
address,
|
|
4365
|
+
kind,
|
|
4366
|
+
chain,
|
|
4367
|
+
rpcUrls
|
|
4368
|
+
}) {
|
|
4369
|
+
const balance = useBalance(kind, address, chain, rpcUrls);
|
|
3850
4370
|
const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
|
|
3851
4371
|
return /* @__PURE__ */ React3.createElement("div", { style: {
|
|
3852
4372
|
background: "var(--dc-surface)",
|
|
@@ -3854,7 +4374,233 @@ function AddressRow({ label, address }) {
|
|
|
3854
4374
|
borderRadius: 10,
|
|
3855
4375
|
padding: "10px 14px",
|
|
3856
4376
|
marginBottom: 8
|
|
3857
|
-
} }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase",
|
|
4377
|
+
} }, /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase" } }, label), /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, fontFamily: "monospace", color: "var(--dc-text)" } }, balance.loading ? "\u2026" : `${formatBalance(balance.value)} ${balance.symbol}`)), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ React3.createElement(CopyButton, { value: address })));
|
|
4378
|
+
}
|
|
4379
|
+
function SignInView({
|
|
4380
|
+
onGoogleSignIn,
|
|
4381
|
+
onEmailStart,
|
|
4382
|
+
onEmailVerify,
|
|
4383
|
+
authMethods,
|
|
4384
|
+
resumeHint
|
|
4385
|
+
}) {
|
|
4386
|
+
const [googleLoading, setGoogleLoading] = useState2(false);
|
|
4387
|
+
const [email, setEmail] = useState2("");
|
|
4388
|
+
const [code, setCode] = useState2("");
|
|
4389
|
+
const [emailSent, setEmailSent] = useState2(false);
|
|
4390
|
+
const [emailLoading, setEmailLoading] = useState2(false);
|
|
4391
|
+
const [error, setError] = useState2("");
|
|
4392
|
+
async function handleGoogle() {
|
|
4393
|
+
setError("");
|
|
4394
|
+
setGoogleLoading(true);
|
|
4395
|
+
try {
|
|
4396
|
+
await onGoogleSignIn();
|
|
4397
|
+
} catch (e) {
|
|
4398
|
+
setError(e.message);
|
|
4399
|
+
setGoogleLoading(false);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
async function handleEmailStart(e) {
|
|
4403
|
+
e.preventDefault();
|
|
4404
|
+
setError("");
|
|
4405
|
+
setEmailLoading(true);
|
|
4406
|
+
try {
|
|
4407
|
+
await onEmailStart(email);
|
|
4408
|
+
setEmailSent(true);
|
|
4409
|
+
} catch (e2) {
|
|
4410
|
+
setError(e2.message);
|
|
4411
|
+
} finally {
|
|
4412
|
+
setEmailLoading(false);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
async function handleEmailVerify(e) {
|
|
4416
|
+
e.preventDefault();
|
|
4417
|
+
setError("");
|
|
4418
|
+
setEmailLoading(true);
|
|
4419
|
+
try {
|
|
4420
|
+
await onEmailVerify(email, code);
|
|
4421
|
+
} catch (e2) {
|
|
4422
|
+
setError(e2.message);
|
|
4423
|
+
} finally {
|
|
4424
|
+
setEmailLoading(false);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
const inputStyle = {
|
|
4428
|
+
width: "100%",
|
|
4429
|
+
background: "var(--dc-surface)",
|
|
4430
|
+
border: "1px solid var(--dc-border)",
|
|
4431
|
+
borderRadius: 8,
|
|
4432
|
+
color: "var(--dc-text)",
|
|
4433
|
+
padding: "9px 12px",
|
|
4434
|
+
fontSize: 13,
|
|
4435
|
+
outline: "none",
|
|
4436
|
+
fontFamily: "inherit",
|
|
4437
|
+
boxSizing: "border-box"
|
|
4438
|
+
};
|
|
4439
|
+
return /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 16px" } }, resumeHint && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 12, color: "var(--dc-text-muted)", marginBottom: 12, textAlign: "center" } }, "Welcome back \u2014 sign in again to resume ", resumeHint), authMethods.includes("google") && /* @__PURE__ */ React3.createElement(
|
|
4440
|
+
"button",
|
|
4441
|
+
{
|
|
4442
|
+
onClick: handleGoogle,
|
|
4443
|
+
disabled: googleLoading,
|
|
4444
|
+
style: {
|
|
4445
|
+
width: "100%",
|
|
4446
|
+
padding: "10px 0",
|
|
4447
|
+
marginBottom: 10,
|
|
4448
|
+
background: "var(--dc-accent)",
|
|
4449
|
+
color: "var(--dc-accent-text)",
|
|
4450
|
+
border: "none",
|
|
4451
|
+
borderRadius: 10,
|
|
4452
|
+
fontSize: 13,
|
|
4453
|
+
fontWeight: 700,
|
|
4454
|
+
cursor: googleLoading ? "not-allowed" : "pointer",
|
|
4455
|
+
opacity: googleLoading ? 0.7 : 1,
|
|
4456
|
+
fontFamily: "inherit"
|
|
4457
|
+
}
|
|
4458
|
+
},
|
|
4459
|
+
googleLoading ? "Redirecting\u2026" : "Continue with Google"
|
|
4460
|
+
), authMethods.includes("email") && /* @__PURE__ */ React3.createElement(React3.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
|
|
4461
|
+
"input",
|
|
4462
|
+
{
|
|
4463
|
+
type: "email",
|
|
4464
|
+
required: true,
|
|
4465
|
+
placeholder: "your@email.com",
|
|
4466
|
+
value: email,
|
|
4467
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4468
|
+
style: { ...inputStyle, flex: 1 }
|
|
4469
|
+
}
|
|
4470
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4471
|
+
padding: "0 16px",
|
|
4472
|
+
background: "var(--dc-surface)",
|
|
4473
|
+
border: "1px solid var(--dc-border)",
|
|
4474
|
+
borderRadius: 8,
|
|
4475
|
+
color: "var(--dc-text)",
|
|
4476
|
+
fontSize: 13,
|
|
4477
|
+
cursor: "pointer",
|
|
4478
|
+
fontFamily: "inherit"
|
|
4479
|
+
} }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
|
|
4480
|
+
"input",
|
|
4481
|
+
{
|
|
4482
|
+
placeholder: "6-digit code",
|
|
4483
|
+
value: code,
|
|
4484
|
+
maxLength: 6,
|
|
4485
|
+
autoFocus: true,
|
|
4486
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
|
|
4487
|
+
style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
|
|
4488
|
+
}
|
|
4489
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4490
|
+
padding: "0 16px",
|
|
4491
|
+
background: "var(--dc-accent)",
|
|
4492
|
+
color: "var(--dc-accent-text)",
|
|
4493
|
+
border: "none",
|
|
4494
|
+
borderRadius: 8,
|
|
4495
|
+
fontSize: 13,
|
|
4496
|
+
fontWeight: 700,
|
|
4497
|
+
cursor: "pointer",
|
|
4498
|
+
fontFamily: "inherit"
|
|
4499
|
+
} }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
|
|
4500
|
+
}
|
|
4501
|
+
function RecoverySection({
|
|
4502
|
+
addresses,
|
|
4503
|
+
onRotateShares,
|
|
4504
|
+
onExportPortableBackup
|
|
4505
|
+
}) {
|
|
4506
|
+
const [expanded, setExpanded] = useState2(false);
|
|
4507
|
+
const [mode, setMode] = useState2(null);
|
|
4508
|
+
const [password, setPassword] = useState2("");
|
|
4509
|
+
const [passwordHint, setPasswordHint] = useState2("");
|
|
4510
|
+
const [confirmed, setConfirmed] = useState2(false);
|
|
4511
|
+
const [busy, setBusy] = useState2(false);
|
|
4512
|
+
const [result, setResult] = useState2(null);
|
|
4513
|
+
const [error, setError] = useState2("");
|
|
4514
|
+
function reset() {
|
|
4515
|
+
setMode(null);
|
|
4516
|
+
setPassword("");
|
|
4517
|
+
setPasswordHint("");
|
|
4518
|
+
setConfirmed(false);
|
|
4519
|
+
setError("");
|
|
4520
|
+
}
|
|
4521
|
+
async function handleGenerate() {
|
|
4522
|
+
if (!addresses?.evm || !mode) return;
|
|
4523
|
+
if (password.length < 8) {
|
|
4524
|
+
setError("Password must be at least 8 characters.");
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
if (mode === "portable" && !confirmed) {
|
|
4528
|
+
setError("Confirm you understand the risk first.");
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
setBusy(true);
|
|
4532
|
+
setError("");
|
|
4533
|
+
try {
|
|
4534
|
+
if (mode === "share") {
|
|
4535
|
+
const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
|
|
4536
|
+
downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4537
|
+
setResult({ kind: "share", version: file.shareSetVersion });
|
|
4538
|
+
} else {
|
|
4539
|
+
const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
|
|
4540
|
+
downloadJson(file, `decane-PORTABLE-BACKUP-${addresses.evm.slice(0, 8)}.json`);
|
|
4541
|
+
setResult({ kind: "portable", version: file.shareSetVersion });
|
|
4542
|
+
}
|
|
4543
|
+
reset();
|
|
4544
|
+
} catch (e) {
|
|
4545
|
+
setError(e.message);
|
|
4546
|
+
} finally {
|
|
4547
|
+
setBusy(false);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
const inputStyle = {
|
|
4551
|
+
width: "100%",
|
|
4552
|
+
background: "var(--dc-bg)",
|
|
4553
|
+
border: "1px solid var(--dc-border)",
|
|
4554
|
+
borderRadius: 8,
|
|
4555
|
+
color: "var(--dc-text)",
|
|
4556
|
+
padding: "8px 10px",
|
|
4557
|
+
fontSize: 12,
|
|
4558
|
+
outline: "none",
|
|
4559
|
+
fontFamily: "inherit",
|
|
4560
|
+
boxSizing: "border-box"
|
|
4561
|
+
};
|
|
4562
|
+
const btnStyle = {
|
|
4563
|
+
padding: "8px 12px",
|
|
4564
|
+
borderRadius: 8,
|
|
4565
|
+
fontSize: 12,
|
|
4566
|
+
fontWeight: 700,
|
|
4567
|
+
cursor: "pointer",
|
|
4568
|
+
fontFamily: "inherit",
|
|
4569
|
+
border: "none"
|
|
4570
|
+
};
|
|
4571
|
+
return /* @__PURE__ */ React3.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ React3.createElement(
|
|
4572
|
+
"button",
|
|
4573
|
+
{
|
|
4574
|
+
onClick: () => setExpanded((v) => !v),
|
|
4575
|
+
style: {
|
|
4576
|
+
width: "100%",
|
|
4577
|
+
padding: "10px 14px",
|
|
4578
|
+
background: "var(--dc-surface)",
|
|
4579
|
+
border: "none",
|
|
4580
|
+
display: "flex",
|
|
4581
|
+
alignItems: "center",
|
|
4582
|
+
justifyContent: "space-between",
|
|
4583
|
+
cursor: "pointer",
|
|
4584
|
+
fontFamily: "inherit"
|
|
4585
|
+
}
|
|
4586
|
+
},
|
|
4587
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
|
|
4588
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
|
|
4589
|
+
), expanded && /* @__PURE__ */ React3.createElement("div", { style: { padding: "12px 14px", borderTop: "1px solid var(--dc-border)" } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5, margin: "0 0 10px" } }, "Your wallet is split into three shares \u2014 device, server, and one that's yours to keep. Any two reconstruct it; one alone reveals nothing."), !mode && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: () => {
|
|
4590
|
+
reset();
|
|
4591
|
+
setMode("share");
|
|
4592
|
+
}, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ React3.createElement("button", { onClick: () => {
|
|
4593
|
+
reset();
|
|
4594
|
+
setMode("portable");
|
|
4595
|
+
}, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate portable backup (Decane-is-down)")), mode === "share" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", margin: 0 } }, "One share \u2014 useless alone, safe to keep in a password manager or cloud drive. Generating a new one invalidates any previous recovery-share file."), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), mode === "portable" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4596
|
+
background: "rgba(255,85,51,0.08)",
|
|
4597
|
+
border: "1px solid rgba(255,85,51,0.3)",
|
|
4598
|
+
borderRadius: 8,
|
|
4599
|
+
padding: "8px 10px",
|
|
4600
|
+
fontSize: 11,
|
|
4601
|
+
color: "#ff9977",
|
|
4602
|
+
lineHeight: 1.5
|
|
4603
|
+
} }, /* @__PURE__ */ React3.createElement("strong", { style: { display: "block", marginBottom: 3, color: "#ff5533" } }, "This file controls your wallet"), "Contains two shares \u2014 file plus password gives complete control. Keep it offline, not a cloud drive. This also invalidates your current recovery-share file."), /* @__PURE__ */ React3.createElement("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--dc-text)" } }, /* @__PURE__ */ React3.createElement("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), style: { width: "auto" } }), "I understand this is a bearer secret"), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy || !confirmed, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), result && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5 } }, result.kind === "share" ? "Recovery-share file" : "Portable backup", " downloaded \u2014 share set is now version ", result.version, ". Any previous recovery file no longer works.")));
|
|
3858
4604
|
}
|
|
3859
4605
|
function SocialWalletModal({
|
|
3860
4606
|
open,
|
|
@@ -3862,7 +4608,15 @@ function SocialWalletModal({
|
|
|
3862
4608
|
theme = "auto",
|
|
3863
4609
|
socialAuth,
|
|
3864
4610
|
onUnlock,
|
|
3865
|
-
onDisconnect
|
|
4611
|
+
onDisconnect,
|
|
4612
|
+
onGoogleSignIn,
|
|
4613
|
+
onEmailStart,
|
|
4614
|
+
onEmailVerify,
|
|
4615
|
+
authMethods = ["google", "email"],
|
|
4616
|
+
onRotateShares,
|
|
4617
|
+
onExportPortableBackup,
|
|
4618
|
+
rpcUrls,
|
|
4619
|
+
chains
|
|
3866
4620
|
}) {
|
|
3867
4621
|
const [timer, setTimer] = useState2("");
|
|
3868
4622
|
const [unlocking, setUnlocking] = useState2(false);
|
|
@@ -3911,6 +4665,9 @@ function SocialWalletModal({
|
|
|
3911
4665
|
setDisconnecting(false);
|
|
3912
4666
|
}
|
|
3913
4667
|
}
|
|
4668
|
+
const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
|
|
4669
|
+
const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
4670
|
+
const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
|
|
3914
4671
|
const modal = /* @__PURE__ */ React3.createElement(
|
|
3915
4672
|
"div",
|
|
3916
4673
|
{
|
|
@@ -3948,10 +4705,14 @@ function SocialWalletModal({
|
|
|
3948
4705
|
borderRadius: 20,
|
|
3949
4706
|
width: "100%",
|
|
3950
4707
|
maxWidth: 380,
|
|
3951
|
-
|
|
3952
|
-
|
|
4708
|
+
maxHeight: "90vh",
|
|
4709
|
+
overflowY: "auto",
|
|
4710
|
+
boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
3953
4711
|
} }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
3954
4712
|
height: 2,
|
|
4713
|
+
position: "sticky",
|
|
4714
|
+
top: 0,
|
|
4715
|
+
zIndex: 1,
|
|
3955
4716
|
background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
|
|
3956
4717
|
} }), /* @__PURE__ */ React3.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
3957
4718
|
width: 34,
|
|
@@ -3980,7 +4741,7 @@ function SocialWalletModal({
|
|
|
3980
4741
|
strokeLinecap: "round",
|
|
3981
4742
|
strokeLinejoin: "round"
|
|
3982
4743
|
}
|
|
3983
|
-
))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split
|
|
4744
|
+
))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split three ways")), /* @__PURE__ */ React3.createElement(
|
|
3984
4745
|
"button",
|
|
3985
4746
|
{
|
|
3986
4747
|
onClick: onClose,
|
|
@@ -4000,7 +4761,16 @@ function SocialWalletModal({
|
|
|
4000
4761
|
}
|
|
4001
4762
|
},
|
|
4002
4763
|
/* @__PURE__ */ React3.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ React3.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
|
|
4003
|
-
)), /* @__PURE__ */ React3.createElement(
|
|
4764
|
+
)), showSignIn ? /* @__PURE__ */ React3.createElement(
|
|
4765
|
+
SignInView,
|
|
4766
|
+
{
|
|
4767
|
+
onGoogleSignIn,
|
|
4768
|
+
onEmailStart,
|
|
4769
|
+
onEmailVerify,
|
|
4770
|
+
authMethods,
|
|
4771
|
+
resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
|
|
4772
|
+
}
|
|
4773
|
+
) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4004
4774
|
margin: "0 16px 14px",
|
|
4005
4775
|
padding: "8px 14px",
|
|
4006
4776
|
background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
|
|
@@ -4020,7 +4790,14 @@ function SocialWalletModal({
|
|
|
4020
4790
|
fontSize: 12,
|
|
4021
4791
|
fontWeight: 700,
|
|
4022
4792
|
color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
|
|
4023
|
-
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px
|
|
4793
|
+
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 14px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ React3.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm, kind: "evm", chain: evmChain, rpcUrls }), socialAuth.addresses?.solana && /* @__PURE__ */ React3.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana, kind: "solana", chain: solChain, rpcUrls })), socialAuth.isUnlocked && /* @__PURE__ */ React3.createElement(
|
|
4794
|
+
RecoverySection,
|
|
4795
|
+
{
|
|
4796
|
+
addresses: socialAuth.addresses,
|
|
4797
|
+
onRotateShares,
|
|
4798
|
+
onExportPortableBackup
|
|
4799
|
+
}
|
|
4800
|
+
), /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4024
4801
|
padding: "14px 16px",
|
|
4025
4802
|
borderTop: "1px solid var(--dc-border)",
|
|
4026
4803
|
display: "flex",
|
|
@@ -4065,7 +4842,7 @@ function SocialWalletModal({
|
|
|
4065
4842
|
}
|
|
4066
4843
|
},
|
|
4067
4844
|
disconnecting ? "Signing out\u2026" : "Sign out"
|
|
4068
|
-
)))
|
|
4845
|
+
))))
|
|
4069
4846
|
);
|
|
4070
4847
|
return createPortal2(modal, document.body);
|
|
4071
4848
|
}
|
|
@@ -4083,7 +4860,8 @@ var DEFAULT_SOCIAL = {
|
|
|
4083
4860
|
addresses: null,
|
|
4084
4861
|
isNewUser: false,
|
|
4085
4862
|
isUnlocked: false,
|
|
4086
|
-
sessionExpiresAt: null
|
|
4863
|
+
sessionExpiresAt: null,
|
|
4864
|
+
needsReconnect: false
|
|
4087
4865
|
};
|
|
4088
4866
|
var DecaneContext = createContext(null);
|
|
4089
4867
|
function hasSocial(mode) {
|
|
@@ -4143,7 +4921,7 @@ function DecaneKit({ config, children }) {
|
|
|
4143
4921
|
sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
|
|
4144
4922
|
const saved = sdk.getAddresses();
|
|
4145
4923
|
if (saved) {
|
|
4146
|
-
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
|
|
4924
|
+
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
|
|
4147
4925
|
}
|
|
4148
4926
|
const params = new URLSearchParams(window.location.search);
|
|
4149
4927
|
if (params.has("decane_jwt") || params.has("decane_error")) {
|
|
@@ -4157,7 +4935,8 @@ function DecaneKit({ config, children }) {
|
|
|
4157
4935
|
addresses: result.addresses,
|
|
4158
4936
|
isNewUser: result.isNewUser,
|
|
4159
4937
|
isUnlocked: sdk?.isUnlocked() ?? false,
|
|
4160
|
-
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
|
|
4938
|
+
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
|
|
4939
|
+
needsReconnect: false
|
|
4161
4940
|
});
|
|
4162
4941
|
}
|
|
4163
4942
|
const connectWithGoogle = useCallback3(async () => {
|
|
@@ -4184,7 +4963,8 @@ function DecaneKit({ config, children }) {
|
|
|
4184
4963
|
setSocialAuth((p) => ({
|
|
4185
4964
|
...p,
|
|
4186
4965
|
isUnlocked: sdk.isUnlocked(),
|
|
4187
|
-
sessionExpiresAt: sdk.sessionExpiresAt()
|
|
4966
|
+
sessionExpiresAt: sdk.sessionExpiresAt(),
|
|
4967
|
+
needsReconnect: false
|
|
4188
4968
|
}));
|
|
4189
4969
|
}, []);
|
|
4190
4970
|
const socialDisconnect = useCallback3(async () => {
|
|
@@ -4208,6 +4988,31 @@ function DecaneKit({ config, children }) {
|
|
|
4208
4988
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
4209
4989
|
return sdk.signSolanaTransaction(tx);
|
|
4210
4990
|
}, []);
|
|
4991
|
+
const socialRotateShares = useCallback3(async (opts) => {
|
|
4992
|
+
const sdk = socialRef.current;
|
|
4993
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
4994
|
+
return sdk.rotateShares(opts);
|
|
4995
|
+
}, []);
|
|
4996
|
+
const socialExportPortableBackup = useCallback3(async (opts) => {
|
|
4997
|
+
const sdk = socialRef.current;
|
|
4998
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
4999
|
+
return sdk.exportPortableBackup(opts);
|
|
5000
|
+
}, []);
|
|
5001
|
+
const socialHasRecoveryShare = useCallback3(async () => {
|
|
5002
|
+
const sdk = socialRef.current;
|
|
5003
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5004
|
+
return sdk.hasRecoveryShare();
|
|
5005
|
+
}, []);
|
|
5006
|
+
const socialShouldPromptRecovery = useCallback3(async () => {
|
|
5007
|
+
const sdk = socialRef.current;
|
|
5008
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5009
|
+
return sdk.shouldPromptRecovery();
|
|
5010
|
+
}, []);
|
|
5011
|
+
const socialDismissRecoveryPrompt = useCallback3(async () => {
|
|
5012
|
+
const sdk = socialRef.current;
|
|
5013
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5014
|
+
return sdk.dismissRecoveryPrompt();
|
|
5015
|
+
}, []);
|
|
4211
5016
|
const handleWalletConnect = useCallback3(
|
|
4212
5017
|
(wallet, preference) => {
|
|
4213
5018
|
walletRef.current.connect(wallet, preference).catch(console.error);
|
|
@@ -4235,6 +5040,11 @@ function DecaneKit({ config, children }) {
|
|
|
4235
5040
|
socialSignMessage,
|
|
4236
5041
|
socialSendTransaction,
|
|
4237
5042
|
socialSignSolanaTransaction,
|
|
5043
|
+
socialRotateShares,
|
|
5044
|
+
socialExportPortableBackup,
|
|
5045
|
+
socialHasRecoveryShare,
|
|
5046
|
+
socialShouldPromptRecovery,
|
|
5047
|
+
socialDismissRecoveryPrompt,
|
|
4238
5048
|
socialModalOpen,
|
|
4239
5049
|
openSocialModal,
|
|
4240
5050
|
closeSocialModal
|
|
@@ -4265,7 +5075,15 @@ function DecaneKit({ config, children }) {
|
|
|
4265
5075
|
theme,
|
|
4266
5076
|
socialAuth,
|
|
4267
5077
|
onUnlock: socialUnlock,
|
|
4268
|
-
onDisconnect: socialDisconnect
|
|
5078
|
+
onDisconnect: socialDisconnect,
|
|
5079
|
+
onGoogleSignIn: connectWithGoogle,
|
|
5080
|
+
onEmailStart: connectWithEmail,
|
|
5081
|
+
onEmailVerify: verifyEmailCode2,
|
|
5082
|
+
authMethods: config?.social?.authMethods ?? ["google", "email"],
|
|
5083
|
+
onRotateShares: socialRotateShares,
|
|
5084
|
+
onExportPortableBackup: socialExportPortableBackup,
|
|
5085
|
+
rpcUrls: config?.social?.rpcUrls,
|
|
5086
|
+
chains: config?.social?.chains
|
|
4269
5087
|
}
|
|
4270
5088
|
)
|
|
4271
5089
|
);
|
|
@@ -4353,6 +5171,11 @@ function useSocialAuth() {
|
|
|
4353
5171
|
isNewUser: socialAuth.isNewUser,
|
|
4354
5172
|
addresses: socialAuth.addresses,
|
|
4355
5173
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5174
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5175
|
+
// live session — unlock() will always throw in this state (every
|
|
5176
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5177
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5178
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4356
5179
|
googleLoading,
|
|
4357
5180
|
emailLoading,
|
|
4358
5181
|
error,
|
|
@@ -4372,6 +5195,11 @@ function useSocialWallet() {
|
|
|
4372
5195
|
socialSignMessage,
|
|
4373
5196
|
socialSendTransaction,
|
|
4374
5197
|
socialSignSolanaTransaction,
|
|
5198
|
+
socialRotateShares,
|
|
5199
|
+
socialExportPortableBackup,
|
|
5200
|
+
socialHasRecoveryShare,
|
|
5201
|
+
socialShouldPromptRecovery,
|
|
5202
|
+
socialDismissRecoveryPrompt,
|
|
4375
5203
|
openSocialModal,
|
|
4376
5204
|
closeSocialModal,
|
|
4377
5205
|
socialModalOpen
|
|
@@ -4421,6 +5249,26 @@ function useSocialWallet() {
|
|
|
4421
5249
|
throw e;
|
|
4422
5250
|
}
|
|
4423
5251
|
}, [socialDisconnect]);
|
|
5252
|
+
const rotateShares = useCallback4(
|
|
5253
|
+
(opts) => run(() => socialRotateShares(opts)),
|
|
5254
|
+
[socialRotateShares]
|
|
5255
|
+
);
|
|
5256
|
+
const exportPortableBackup = useCallback4(
|
|
5257
|
+
(opts) => run(() => socialExportPortableBackup(opts)),
|
|
5258
|
+
[socialExportPortableBackup]
|
|
5259
|
+
);
|
|
5260
|
+
const hasRecoveryShare = useCallback4(
|
|
5261
|
+
() => run(() => socialHasRecoveryShare()),
|
|
5262
|
+
[socialHasRecoveryShare]
|
|
5263
|
+
);
|
|
5264
|
+
const shouldPromptRecovery = useCallback4(
|
|
5265
|
+
() => run(() => socialShouldPromptRecovery()),
|
|
5266
|
+
[socialShouldPromptRecovery]
|
|
5267
|
+
);
|
|
5268
|
+
const dismissRecoveryPrompt = useCallback4(
|
|
5269
|
+
() => run(() => socialDismissRecoveryPrompt()),
|
|
5270
|
+
[socialDismissRecoveryPrompt]
|
|
5271
|
+
);
|
|
4424
5272
|
return {
|
|
4425
5273
|
// State
|
|
4426
5274
|
isConnected: !!socialAuth.addresses,
|
|
@@ -4428,6 +5276,11 @@ function useSocialWallet() {
|
|
|
4428
5276
|
isNewUser: socialAuth.isNewUser,
|
|
4429
5277
|
addresses: socialAuth.addresses,
|
|
4430
5278
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5279
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5280
|
+
// live session — unlock() will always throw in this state (every
|
|
5281
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5282
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5283
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4431
5284
|
// Modal
|
|
4432
5285
|
modalOpen: socialModalOpen,
|
|
4433
5286
|
openModal: openSocialModal,
|
|
@@ -4439,6 +5292,12 @@ function useSocialWallet() {
|
|
|
4439
5292
|
signMessage,
|
|
4440
5293
|
sendTransaction,
|
|
4441
5294
|
signSolanaTransaction: signSolanaTransaction2,
|
|
5295
|
+
// Recovery (2-of-3 Shamir share set)
|
|
5296
|
+
rotateShares,
|
|
5297
|
+
exportPortableBackup,
|
|
5298
|
+
hasRecoveryShare,
|
|
5299
|
+
shouldPromptRecovery,
|
|
5300
|
+
dismissRecoveryPrompt,
|
|
4442
5301
|
loading,
|
|
4443
5302
|
error,
|
|
4444
5303
|
clearError: () => setError(null)
|
|
@@ -4676,14 +5535,17 @@ export {
|
|
|
4676
5535
|
DecaneError,
|
|
4677
5536
|
DecaneErrorCode,
|
|
4678
5537
|
DecaneKit,
|
|
5538
|
+
ExportNotAuthorizedError,
|
|
4679
5539
|
InvalidApiKeyError,
|
|
4680
|
-
InvalidPhraseError,
|
|
4681
5540
|
InvalidPinError,
|
|
5541
|
+
InvalidRecoveryFileError,
|
|
4682
5542
|
NetworkError,
|
|
4683
5543
|
NewDeviceError,
|
|
4684
5544
|
OriginNotAllowedError,
|
|
4685
5545
|
PromptPinNotConfiguredError,
|
|
5546
|
+
ReconnectRequiredError,
|
|
4686
5547
|
RecoveryFailedError,
|
|
5548
|
+
RecoveryWalletMismatchError,
|
|
4687
5549
|
SessionExpiredError,
|
|
4688
5550
|
ShareCorruptedError,
|
|
4689
5551
|
SocialWalletModal,
|
|
@@ -4691,6 +5553,7 @@ export {
|
|
|
4691
5553
|
UserCancelledError,
|
|
4692
5554
|
WalletSelector,
|
|
4693
5555
|
WebAuthnNotSupportedError,
|
|
5556
|
+
WrongPasswordError,
|
|
4694
5557
|
createDecane,
|
|
4695
5558
|
createDecaneConnect,
|
|
4696
5559
|
startEmailAuth,
|