decane-connect-kit 2.24.0 → 2.25.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 +201 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +207 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -159,6 +159,27 @@ interface DecaneConnectConfig {
|
|
|
159
159
|
* has happened. Default false: the share is wrapped and stored at creation.
|
|
160
160
|
*/
|
|
161
161
|
deferDeviceProtection?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Resume a live session in a NEW TAB, not just after a reload. Default false.
|
|
164
|
+
*
|
|
165
|
+
* The session — handle plus JWT — normally lives in sessionStorage, so it
|
|
166
|
+
* survives a reload and dies with the tab. That makes a second tab, or a
|
|
167
|
+
* return visit an hour later, a full sign-in and unlock again even though the
|
|
168
|
+
* enclave session is still valid for two hours.
|
|
169
|
+
*
|
|
170
|
+
* With this on, the session is held in localStorage under the same expiry, so
|
|
171
|
+
* a returning user is signing again in about a second.
|
|
172
|
+
*
|
|
173
|
+
* THE TRADE, stated plainly: the handle IS the signing credential — no other
|
|
174
|
+
* header is required to sign with it. Widening its storage widens the window
|
|
175
|
+
* in which anything able to run script on your origin could take it, from one
|
|
176
|
+
* tab to every tab until it expires. It is off by default for that reason.
|
|
177
|
+
*
|
|
178
|
+
* Turn it on when the return-visit cost matters more than that window, and
|
|
179
|
+
* pair it with `requireAssertionPerSignature` so a stolen handle still cannot
|
|
180
|
+
* move funds without a fresh authenticator touch.
|
|
181
|
+
*/
|
|
182
|
+
resumeSessionAcrossTabs?: boolean;
|
|
162
183
|
/**
|
|
163
184
|
* Offer to save a recovery-share file at signup. **Default false** since
|
|
164
185
|
* 2.7.4 — opt in explicitly with `true`.
|
package/dist/index.d.ts
CHANGED
|
@@ -159,6 +159,27 @@ interface DecaneConnectConfig {
|
|
|
159
159
|
* has happened. Default false: the share is wrapped and stored at creation.
|
|
160
160
|
*/
|
|
161
161
|
deferDeviceProtection?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Resume a live session in a NEW TAB, not just after a reload. Default false.
|
|
164
|
+
*
|
|
165
|
+
* The session — handle plus JWT — normally lives in sessionStorage, so it
|
|
166
|
+
* survives a reload and dies with the tab. That makes a second tab, or a
|
|
167
|
+
* return visit an hour later, a full sign-in and unlock again even though the
|
|
168
|
+
* enclave session is still valid for two hours.
|
|
169
|
+
*
|
|
170
|
+
* With this on, the session is held in localStorage under the same expiry, so
|
|
171
|
+
* a returning user is signing again in about a second.
|
|
172
|
+
*
|
|
173
|
+
* THE TRADE, stated plainly: the handle IS the signing credential — no other
|
|
174
|
+
* header is required to sign with it. Widening its storage widens the window
|
|
175
|
+
* in which anything able to run script on your origin could take it, from one
|
|
176
|
+
* tab to every tab until it expires. It is off by default for that reason.
|
|
177
|
+
*
|
|
178
|
+
* Turn it on when the return-visit cost matters more than that window, and
|
|
179
|
+
* pair it with `requireAssertionPerSignature` so a stolen handle still cannot
|
|
180
|
+
* move funds without a fresh authenticator touch.
|
|
181
|
+
*/
|
|
182
|
+
resumeSessionAcrossTabs?: boolean;
|
|
162
183
|
/**
|
|
163
184
|
* Offer to save a recovery-share file at signup. **Default false** since
|
|
164
185
|
* 2.7.4 — opt in explicitly with `true`.
|
package/dist/index.js
CHANGED
|
@@ -2331,9 +2331,62 @@ import { sha256, encodeBase58 } from "ethers";
|
|
|
2331
2331
|
// src/social/teeClient.ts
|
|
2332
2332
|
import { startAuthentication } from "@simplewebauthn/browser";
|
|
2333
2333
|
|
|
2334
|
+
// src/social/attestation.ts
|
|
2335
|
+
import {
|
|
2336
|
+
getCollateral,
|
|
2337
|
+
getCollateralAndVerify,
|
|
2338
|
+
verify as verifyQuoteWithCollateral,
|
|
2339
|
+
PHALA_PCCS_URL
|
|
2340
|
+
} from "@phala/dcap-qvl";
|
|
2341
|
+
|
|
2342
|
+
// src/social/collateralCache.ts
|
|
2343
|
+
var CACHE_PREFIX = "decane:collateral:v1:";
|
|
2344
|
+
var COLLATERAL_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
2345
|
+
function storage() {
|
|
2346
|
+
try {
|
|
2347
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2348
|
+
} catch {
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
function keyFor(teeUrl) {
|
|
2353
|
+
return `${CACHE_PREFIX}${teeUrl}`;
|
|
2354
|
+
}
|
|
2355
|
+
function readCollateral(teeUrl) {
|
|
2356
|
+
const store = storage();
|
|
2357
|
+
if (!store) return null;
|
|
2358
|
+
try {
|
|
2359
|
+
const raw = store.getItem(keyFor(teeUrl));
|
|
2360
|
+
if (!raw) return null;
|
|
2361
|
+
const entry = JSON.parse(raw);
|
|
2362
|
+
if (!entry?.at || Date.now() - entry.at > COLLATERAL_TTL_MS) {
|
|
2363
|
+
store.removeItem(keyFor(teeUrl));
|
|
2364
|
+
return null;
|
|
2365
|
+
}
|
|
2366
|
+
return entry.collateral ?? null;
|
|
2367
|
+
} catch {
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function writeCollateral(teeUrl, collateral) {
|
|
2372
|
+
const store = storage();
|
|
2373
|
+
if (!store) return;
|
|
2374
|
+
try {
|
|
2375
|
+
store.setItem(keyFor(teeUrl), JSON.stringify({ at: Date.now(), collateral }));
|
|
2376
|
+
} catch {
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
function dropCollateral(teeUrl) {
|
|
2380
|
+
const store = storage();
|
|
2381
|
+
if (!store) return;
|
|
2382
|
+
try {
|
|
2383
|
+
store.removeItem(keyFor(teeUrl));
|
|
2384
|
+
} catch {
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2334
2388
|
// src/social/attestation.ts
|
|
2335
2389
|
init_errors();
|
|
2336
|
-
import { getCollateralAndVerify } from "@phala/dcap-qvl";
|
|
2337
2390
|
|
|
2338
2391
|
// ../../infra/tee-measurement.json
|
|
2339
2392
|
var tee_measurement_default = {
|
|
@@ -2360,6 +2413,9 @@ function hexToBytes(hex) {
|
|
|
2360
2413
|
function bytesToHex(bytes) {
|
|
2361
2414
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2362
2415
|
}
|
|
2416
|
+
function nowSeconds() {
|
|
2417
|
+
return Math.floor(Date.now() / 1e3);
|
|
2418
|
+
}
|
|
2363
2419
|
async function verifyTeeAttestation(teeUrl, nonce) {
|
|
2364
2420
|
const res = await fetch(`${teeUrl}/attestation`, {
|
|
2365
2421
|
method: "POST",
|
|
@@ -2369,9 +2425,22 @@ async function verifyTeeAttestation(teeUrl, nonce) {
|
|
|
2369
2425
|
if (!res.ok) throw new AttestationError("quote fetch failed");
|
|
2370
2426
|
const { quote, measurement } = await res.json();
|
|
2371
2427
|
let verified;
|
|
2428
|
+
const cached = readCollateral(teeUrl);
|
|
2372
2429
|
try {
|
|
2373
|
-
|
|
2374
|
-
|
|
2430
|
+
if (cached) {
|
|
2431
|
+
try {
|
|
2432
|
+
verified = await verifyQuoteWithCollateral(hexToBytes(quote), cached, nowSeconds());
|
|
2433
|
+
} catch {
|
|
2434
|
+
dropCollateral(teeUrl);
|
|
2435
|
+
verified = await getCollateralAndVerify(hexToBytes(quote));
|
|
2436
|
+
}
|
|
2437
|
+
} else {
|
|
2438
|
+
const collateral = await getCollateral(PHALA_PCCS_URL, hexToBytes(quote));
|
|
2439
|
+
verified = await verifyQuoteWithCollateral(hexToBytes(quote), collateral, nowSeconds());
|
|
2440
|
+
writeCollateral(teeUrl, collateral);
|
|
2441
|
+
}
|
|
2442
|
+
} catch (e) {
|
|
2443
|
+
if (e instanceof AttestationError) throw e;
|
|
2375
2444
|
throw new AttestationError("quote signature invalid");
|
|
2376
2445
|
}
|
|
2377
2446
|
if (verified.status === "Revoked") {
|
|
@@ -2413,14 +2482,51 @@ async function teeFetch(url, init) {
|
|
|
2413
2482
|
}
|
|
2414
2483
|
var ATTESTATION_TTL_MS = 10 * 6e4;
|
|
2415
2484
|
var attestationCache = /* @__PURE__ */ new Map();
|
|
2485
|
+
var ATTESTATION_PERSIST_PREFIX = "decane:attested:v1:";
|
|
2486
|
+
function persistedAt(teeUrl) {
|
|
2487
|
+
try {
|
|
2488
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
2489
|
+
const raw = sessionStorage.getItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2490
|
+
const at = raw ? Number(raw) : NaN;
|
|
2491
|
+
return Number.isFinite(at) ? at : null;
|
|
2492
|
+
} catch {
|
|
2493
|
+
return null;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function rememberAttested(teeUrl, at) {
|
|
2497
|
+
try {
|
|
2498
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2499
|
+
sessionStorage.setItem(ATTESTATION_PERSIST_PREFIX + teeUrl, String(at));
|
|
2500
|
+
}
|
|
2501
|
+
} catch {
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function forgetAttested(teeUrl) {
|
|
2505
|
+
try {
|
|
2506
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2507
|
+
sessionStorage.removeItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2508
|
+
}
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2416
2512
|
function attestOnce(teeUrl, verify) {
|
|
2417
2513
|
const hit = attestationCache.get(teeUrl);
|
|
2418
2514
|
if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
|
|
2419
|
-
const
|
|
2515
|
+
const at = persistedAt(teeUrl);
|
|
2516
|
+
if (at && Date.now() - at < ATTESTATION_TTL_MS) {
|
|
2517
|
+
const resolved = Promise.resolve();
|
|
2518
|
+
attestationCache.set(teeUrl, { at, promise: resolved });
|
|
2519
|
+
return resolved;
|
|
2520
|
+
}
|
|
2521
|
+
const startedAt = Date.now();
|
|
2522
|
+
const promise = verify().then(() => {
|
|
2523
|
+
rememberAttested(teeUrl, startedAt);
|
|
2524
|
+
}).catch((e) => {
|
|
2420
2525
|
attestationCache.delete(teeUrl);
|
|
2526
|
+
forgetAttested(teeUrl);
|
|
2421
2527
|
throw e;
|
|
2422
2528
|
});
|
|
2423
|
-
attestationCache.set(teeUrl, { at:
|
|
2529
|
+
attestationCache.set(teeUrl, { at: startedAt, promise });
|
|
2424
2530
|
return promise;
|
|
2425
2531
|
}
|
|
2426
2532
|
function prewarmAttestation(teeUrl) {
|
|
@@ -3148,7 +3254,9 @@ function loginWithKingsChatPopup(opts) {
|
|
|
3148
3254
|
}
|
|
3149
3255
|
|
|
3150
3256
|
// src/social/signers/evm.ts
|
|
3257
|
+
init_errors();
|
|
3151
3258
|
import { keccak256, Transaction, resolveProperties } from "ethers";
|
|
3259
|
+
var DECODE = (bytes) => new TextDecoder().decode(bytes);
|
|
3152
3260
|
async function signEvmTransaction(tee, chain, tx, jwt) {
|
|
3153
3261
|
const resolved = await resolveProperties(tx);
|
|
3154
3262
|
const unsigned = Transaction.from(resolved).unsignedSerialized;
|
|
@@ -3157,7 +3265,44 @@ async function signEvmTransaction(tee, chain, tx, jwt) {
|
|
|
3157
3265
|
return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
|
|
3158
3266
|
}
|
|
3159
3267
|
async function signEvmMessage(tee, chain, message, jwt) {
|
|
3160
|
-
return tee.sign({ chain, kind: "message", payload: message }, jwt);
|
|
3268
|
+
return tee.sign({ chain, kind: "message", payload: messageAsString(message) }, jwt);
|
|
3269
|
+
}
|
|
3270
|
+
function messageAsString(message) {
|
|
3271
|
+
if (typeof message === "string") return message;
|
|
3272
|
+
if (!isValidUtf8(message)) {
|
|
3273
|
+
throw new CallSequenceError(
|
|
3274
|
+
"signMessage: the message bytes are not valid UTF-8. The signer hashes text; pass a string, or UTF-8 bytes."
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
return DECODE(message);
|
|
3278
|
+
}
|
|
3279
|
+
function isValidUtf8(b) {
|
|
3280
|
+
let i = 0;
|
|
3281
|
+
while (i < b.length) {
|
|
3282
|
+
const c = b[i];
|
|
3283
|
+
let n = 0;
|
|
3284
|
+
if (c < 128) n = 0;
|
|
3285
|
+
else if (c >= 194 && c <= 223) n = 1;
|
|
3286
|
+
else if (c >= 224 && c <= 239) n = 2;
|
|
3287
|
+
else if (c >= 240 && c <= 244) n = 3;
|
|
3288
|
+
else return false;
|
|
3289
|
+
for (let k = 1; k <= n; k++) {
|
|
3290
|
+
const t = b[i + k];
|
|
3291
|
+
if (t === void 0 || (t & 192) !== 128) return false;
|
|
3292
|
+
}
|
|
3293
|
+
if (n === 2) {
|
|
3294
|
+
const c1 = b[i + 1];
|
|
3295
|
+
if (c === 224 && c1 < 160) return false;
|
|
3296
|
+
if (c === 237 && c1 >= 160) return false;
|
|
3297
|
+
}
|
|
3298
|
+
if (n === 3) {
|
|
3299
|
+
const c1 = b[i + 1];
|
|
3300
|
+
if (c === 240 && c1 < 144) return false;
|
|
3301
|
+
if (c === 244 && c1 >= 144) return false;
|
|
3302
|
+
}
|
|
3303
|
+
i += n + 1;
|
|
3304
|
+
}
|
|
3305
|
+
return true;
|
|
3161
3306
|
}
|
|
3162
3307
|
async function signEvmAuthorization(tee, chainId, address, nonce, jwt) {
|
|
3163
3308
|
const serialized = await tee.sign(
|
|
@@ -3653,29 +3798,44 @@ function clearLastUser(key) {
|
|
|
3653
3798
|
function sessionPersistKey(apiKey) {
|
|
3654
3799
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3655
3800
|
}
|
|
3801
|
+
var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3802
|
+
function useCrossTabSessionStore(enabled) {
|
|
3803
|
+
sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3804
|
+
}
|
|
3656
3805
|
function loadSession(key) {
|
|
3657
3806
|
try {
|
|
3658
|
-
const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
3659
|
-
|
|
3807
|
+
const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
|
|
3808
|
+
if (!raw) return null;
|
|
3809
|
+
const parsed = JSON.parse(raw);
|
|
3810
|
+
if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
|
|
3811
|
+
clearSessionPersist(key);
|
|
3812
|
+
return null;
|
|
3813
|
+
}
|
|
3814
|
+
return parsed;
|
|
3660
3815
|
} catch {
|
|
3661
3816
|
return null;
|
|
3662
3817
|
}
|
|
3663
3818
|
}
|
|
3664
3819
|
function saveSession(key, s) {
|
|
3665
3820
|
try {
|
|
3666
|
-
|
|
3821
|
+
sessionStore()?.setItem(key, JSON.stringify(s));
|
|
3667
3822
|
} catch {
|
|
3668
3823
|
}
|
|
3669
3824
|
}
|
|
3670
3825
|
function clearSessionPersist(key) {
|
|
3671
3826
|
try {
|
|
3672
|
-
sessionStorage.removeItem(key);
|
|
3827
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
|
|
3828
|
+
} catch {
|
|
3829
|
+
}
|
|
3830
|
+
try {
|
|
3831
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
3673
3832
|
} catch {
|
|
3674
3833
|
}
|
|
3675
3834
|
}
|
|
3676
3835
|
async function createDecaneConnect(config) {
|
|
3677
3836
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3678
3837
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3838
|
+
useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
|
|
3679
3839
|
if (!config.chains?.length)
|
|
3680
3840
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3681
3841
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3694,7 +3854,7 @@ async function createDecaneConnect(config) {
|
|
|
3694
3854
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3695
3855
|
const events = new EventEmitter();
|
|
3696
3856
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
3697
|
-
const
|
|
3857
|
+
const storage2 = new Storage();
|
|
3698
3858
|
const session = new SessionManager(
|
|
3699
3859
|
events,
|
|
3700
3860
|
durationMs,
|
|
@@ -3837,7 +3997,7 @@ async function createDecaneConnect(config) {
|
|
|
3837
3997
|
}
|
|
3838
3998
|
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3839
3999
|
try {
|
|
3840
|
-
const stored = await
|
|
4000
|
+
const stored = await storage2.getShare(userId);
|
|
3841
4001
|
if (!stored) return;
|
|
3842
4002
|
const { token } = await api.issueUnlockToken(jwt);
|
|
3843
4003
|
const unlockTokenIv = generateRandomBytes(12);
|
|
@@ -3846,13 +4006,13 @@ async function createDecaneConnect(config) {
|
|
|
3846
4006
|
new TextEncoder().encode(token),
|
|
3847
4007
|
unlockTokenIv
|
|
3848
4008
|
);
|
|
3849
|
-
await
|
|
4009
|
+
await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3850
4010
|
} catch (e) {
|
|
3851
4011
|
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3852
4012
|
}
|
|
3853
4013
|
}
|
|
3854
4014
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3855
|
-
const stored = await
|
|
4015
|
+
const stored = await storage2.getShare(userId);
|
|
3856
4016
|
if (!stored) {
|
|
3857
4017
|
throw new CallSequenceError(
|
|
3858
4018
|
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
@@ -3861,21 +4021,21 @@ async function createDecaneConnect(config) {
|
|
|
3861
4021
|
const wrappingKey = await getWrappingKey(stored);
|
|
3862
4022
|
const iv = generateRandomBytes(12);
|
|
3863
4023
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3864
|
-
await
|
|
4024
|
+
await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3865
4025
|
}
|
|
3866
4026
|
function hasPasswordUnlock(stored) {
|
|
3867
4027
|
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3868
4028
|
}
|
|
3869
4029
|
async function passwordDeviceCandidate() {
|
|
3870
4030
|
if (persisted?.userId) {
|
|
3871
|
-
const stored = await
|
|
4031
|
+
const stored = await storage2.getShare(persisted.userId);
|
|
3872
4032
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3873
4033
|
}
|
|
3874
4034
|
if (lastUserId) {
|
|
3875
|
-
const stored = await
|
|
4035
|
+
const stored = await storage2.getShare(lastUserId);
|
|
3876
4036
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3877
4037
|
}
|
|
3878
|
-
const candidates = (await
|
|
4038
|
+
const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
|
|
3879
4039
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3880
4040
|
}
|
|
3881
4041
|
async function getWrappingKey(stored) {
|
|
@@ -3903,7 +4063,7 @@ async function createDecaneConnect(config) {
|
|
|
3903
4063
|
let credentialId;
|
|
3904
4064
|
let prfOutput;
|
|
3905
4065
|
try {
|
|
3906
|
-
const label = (await
|
|
4066
|
+
const label = (await storage2.getShare(userId))?.profile?.email;
|
|
3907
4067
|
({ credentialId, prfOutput } = await registerWebAuthnCredential({
|
|
3908
4068
|
api,
|
|
3909
4069
|
jwt,
|
|
@@ -3935,8 +4095,8 @@ async function createDecaneConnect(config) {
|
|
|
3935
4095
|
const wrappingKey = await importAesKey(prfOutput);
|
|
3936
4096
|
const iv = generateRandomBytes(12);
|
|
3937
4097
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3938
|
-
const existing = await
|
|
3939
|
-
await
|
|
4098
|
+
const existing = await storage2.getShare(userId);
|
|
4099
|
+
await storage2.putShare({
|
|
3940
4100
|
...existing,
|
|
3941
4101
|
userId,
|
|
3942
4102
|
wrappedDeviceShare,
|
|
@@ -4020,7 +4180,7 @@ async function createDecaneConnect(config) {
|
|
|
4020
4180
|
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
4021
4181
|
const iv = generateRandomBytes(12);
|
|
4022
4182
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4023
|
-
await
|
|
4183
|
+
await storage2.putShare({
|
|
4024
4184
|
userId,
|
|
4025
4185
|
wrappedDeviceShare,
|
|
4026
4186
|
iv,
|
|
@@ -4094,7 +4254,7 @@ async function createDecaneConnect(config) {
|
|
|
4094
4254
|
shareSet.device,
|
|
4095
4255
|
iv
|
|
4096
4256
|
);
|
|
4097
|
-
await
|
|
4257
|
+
await storage2.putShare({
|
|
4098
4258
|
userId,
|
|
4099
4259
|
wrappedDeviceShare,
|
|
4100
4260
|
iv,
|
|
@@ -4198,7 +4358,7 @@ async function createDecaneConnect(config) {
|
|
|
4198
4358
|
async function returningUserUnlock(userId, jwt) {
|
|
4199
4359
|
const rawAddresses = await api.getAddresses(jwt);
|
|
4200
4360
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|
|
4201
|
-
const stored = await
|
|
4361
|
+
const stored = await storage2.getShare(userId);
|
|
4202
4362
|
let deviceShare;
|
|
4203
4363
|
let joinedViaPasskeySync = false;
|
|
4204
4364
|
if (!stored) {
|
|
@@ -4237,7 +4397,7 @@ async function createDecaneConnect(config) {
|
|
|
4237
4397
|
rpId,
|
|
4238
4398
|
teeUrl,
|
|
4239
4399
|
api,
|
|
4240
|
-
storage,
|
|
4400
|
+
storage: storage2,
|
|
4241
4401
|
onProgress: config.onRecoveryProgress,
|
|
4242
4402
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
4243
4403
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
@@ -4251,7 +4411,7 @@ async function createDecaneConnect(config) {
|
|
|
4251
4411
|
});
|
|
4252
4412
|
deviceShare = recovered.deviceShare;
|
|
4253
4413
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4254
|
-
if (!deferDeviceProtection && !await
|
|
4414
|
+
if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4255
4415
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4256
4416
|
}
|
|
4257
4417
|
} else {
|
|
@@ -4272,7 +4432,7 @@ async function createDecaneConnect(config) {
|
|
|
4272
4432
|
} catch (e) {
|
|
4273
4433
|
if (e instanceof StaleDeviceShareError) {
|
|
4274
4434
|
zeroize(deviceShare);
|
|
4275
|
-
await
|
|
4435
|
+
await storage2.deleteShare(userId);
|
|
4276
4436
|
if (stored) return returningUserUnlock(userId, jwt);
|
|
4277
4437
|
} else {
|
|
4278
4438
|
zeroize(deviceShare);
|
|
@@ -4296,7 +4456,7 @@ async function createDecaneConnect(config) {
|
|
|
4296
4456
|
const result = await tee.rotateShares(jwt);
|
|
4297
4457
|
try {
|
|
4298
4458
|
await persistRotatedDeviceShare(userId, result.deviceShare);
|
|
4299
|
-
const refreshed = await
|
|
4459
|
+
const refreshed = await storage2.getShare(userId);
|
|
4300
4460
|
if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
|
|
4301
4461
|
await api.putRecoveryBackup(jwt, {
|
|
4302
4462
|
wrappedDeviceShare: refreshed.wrappedDeviceShare,
|
|
@@ -4329,13 +4489,13 @@ async function createDecaneConnect(config) {
|
|
|
4329
4489
|
}
|
|
4330
4490
|
async function passkeyLoginCandidate() {
|
|
4331
4491
|
if (!persisted?.userId || !isPasskeySupported()) return null;
|
|
4332
|
-
return usablePasskeyShare(await
|
|
4492
|
+
return usablePasskeyShare(await storage2.getShare(persisted.userId));
|
|
4333
4493
|
}
|
|
4334
4494
|
async function passkeyDeviceCandidate() {
|
|
4335
4495
|
if (!isPasskeySupported()) return null;
|
|
4336
4496
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4337
|
-
if (lastUserId) return usablePasskeyShare(await
|
|
4338
|
-
const candidates = (await
|
|
4497
|
+
if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
|
|
4498
|
+
const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4339
4499
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4340
4500
|
}
|
|
4341
4501
|
async function canOfferPasskey() {
|
|
@@ -4343,15 +4503,15 @@ async function createDecaneConnect(config) {
|
|
|
4343
4503
|
if (await passkeyDeviceCandidate()) return true;
|
|
4344
4504
|
const known = persisted?.userId ?? lastUserId;
|
|
4345
4505
|
if (!known) return false;
|
|
4346
|
-
return await
|
|
4506
|
+
return await storage2.getShare(known) == null;
|
|
4347
4507
|
}
|
|
4348
4508
|
async function rememberProfile(userId, profile) {
|
|
4349
4509
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4350
4510
|
if (!Object.keys(incoming).length) return;
|
|
4351
4511
|
try {
|
|
4352
|
-
const stored = await
|
|
4512
|
+
const stored = await storage2.getShare(userId);
|
|
4353
4513
|
if (!stored) return;
|
|
4354
|
-
await
|
|
4514
|
+
await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4355
4515
|
} catch {
|
|
4356
4516
|
}
|
|
4357
4517
|
}
|
|
@@ -4375,7 +4535,7 @@ async function createDecaneConnect(config) {
|
|
|
4375
4535
|
savePersisted(PERSIST_KEY, persisted);
|
|
4376
4536
|
events.emit("connected");
|
|
4377
4537
|
persistLiveSession();
|
|
4378
|
-
const stored = await
|
|
4538
|
+
const stored = await storage2.getShare(userId);
|
|
4379
4539
|
return {
|
|
4380
4540
|
addresses,
|
|
4381
4541
|
isNewUser: false,
|
|
@@ -4548,7 +4708,7 @@ async function createDecaneConnect(config) {
|
|
|
4548
4708
|
events.emit("connected");
|
|
4549
4709
|
persistLiveSession();
|
|
4550
4710
|
scheduleAccessTokenExpiry(token, claims.expiresAt);
|
|
4551
|
-
const stored = await
|
|
4711
|
+
const stored = await storage2.getShare(claims.userId);
|
|
4552
4712
|
return {
|
|
4553
4713
|
addresses,
|
|
4554
4714
|
isNewUser: false,
|
|
@@ -4578,7 +4738,7 @@ async function createDecaneConnect(config) {
|
|
|
4578
4738
|
const candidate = await passkeyDeviceCandidate();
|
|
4579
4739
|
if (candidate) return passkeyLogin(candidate);
|
|
4580
4740
|
const known = persisted?.userId ?? lastUserId;
|
|
4581
|
-
if (isPasskeySupported() && known && await
|
|
4741
|
+
if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
|
|
4582
4742
|
return passkeyLoginWithoutShare(known);
|
|
4583
4743
|
}
|
|
4584
4744
|
throw new NewDeviceError();
|
|
@@ -4597,7 +4757,7 @@ async function createDecaneConnect(config) {
|
|
|
4597
4757
|
async setUnlockPassword(password) {
|
|
4598
4758
|
if (!auth) throw new ReconnectRequiredError();
|
|
4599
4759
|
validateUnlockPassword(password);
|
|
4600
|
-
const stored = await
|
|
4760
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4601
4761
|
if (!stored) throw new NewDeviceError();
|
|
4602
4762
|
const deviceShare = await aesGcmDecrypt(
|
|
4603
4763
|
await getWrappingKey(stored),
|
|
@@ -4616,7 +4776,7 @@ async function createDecaneConnect(config) {
|
|
|
4616
4776
|
);
|
|
4617
4777
|
const iv = generateRandomBytes(12);
|
|
4618
4778
|
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4619
|
-
await
|
|
4779
|
+
await storage2.putShare({
|
|
4620
4780
|
...stored,
|
|
4621
4781
|
unlockMethod: "password",
|
|
4622
4782
|
wrappedDeviceShare,
|
|
@@ -4696,9 +4856,9 @@ async function createDecaneConnect(config) {
|
|
|
4696
4856
|
async removeUnlockPassword() {
|
|
4697
4857
|
if (!auth) throw new ReconnectRequiredError();
|
|
4698
4858
|
await api.revokeUnlockTokens(auth.jwt);
|
|
4699
|
-
const stored = await
|
|
4859
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4700
4860
|
if (!stored || stored.unlockMethod !== "password") return;
|
|
4701
|
-
await
|
|
4861
|
+
await storage2.putShare({
|
|
4702
4862
|
...stored,
|
|
4703
4863
|
wrappedUnlockToken: void 0,
|
|
4704
4864
|
unlockTokenIv: void 0
|
|
@@ -4706,13 +4866,13 @@ async function createDecaneConnect(config) {
|
|
|
4706
4866
|
},
|
|
4707
4867
|
async hasPasskey() {
|
|
4708
4868
|
if (!auth) return false;
|
|
4709
|
-
const stored = await
|
|
4869
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4710
4870
|
return stored?.unlockMethod === "passkey";
|
|
4711
4871
|
},
|
|
4712
4872
|
async addPasskey() {
|
|
4713
4873
|
if (!auth) throw new ReconnectRequiredError();
|
|
4714
4874
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4715
|
-
const stored = await
|
|
4875
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4716
4876
|
if (stored?.unlockMethod === "passkey") return;
|
|
4717
4877
|
if (!stored) {
|
|
4718
4878
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4745,11 +4905,11 @@ async function createDecaneConnect(config) {
|
|
|
4745
4905
|
},
|
|
4746
4906
|
async deviceProtected() {
|
|
4747
4907
|
if (!auth) return false;
|
|
4748
|
-
return await
|
|
4908
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4749
4909
|
},
|
|
4750
4910
|
async protectDevice() {
|
|
4751
4911
|
if (!auth) throw new ReconnectRequiredError();
|
|
4752
|
-
if (await
|
|
4912
|
+
if (await storage2.getShare(auth.userId)) return true;
|
|
4753
4913
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4754
4914
|
const tee = new TeeClient(teeUrl);
|
|
4755
4915
|
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
@@ -4759,7 +4919,7 @@ async function createDecaneConnect(config) {
|
|
|
4759
4919
|
} finally {
|
|
4760
4920
|
zeroize(provisioned);
|
|
4761
4921
|
}
|
|
4762
|
-
return await
|
|
4922
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4763
4923
|
},
|
|
4764
4924
|
async unlock() {
|
|
4765
4925
|
if (!auth) {
|
|
@@ -4801,8 +4961,7 @@ async function createDecaneConnect(config) {
|
|
|
4801
4961
|
return bufferToB64u(sig);
|
|
4802
4962
|
}
|
|
4803
4963
|
if (req.chain.startsWith("tron:")) {
|
|
4804
|
-
|
|
4805
|
-
return tee.sign({ chain: req.chain, kind: "message", payload: msg }, auth?.jwt);
|
|
4964
|
+
return tee.sign({ chain: req.chain, kind: "message", payload: messageAsString(req.message) }, auth?.jwt);
|
|
4806
4965
|
}
|
|
4807
4966
|
throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
|
|
4808
4967
|
},
|