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