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