decane-connect-kit 2.24.1 → 2.26.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 +206 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -1
- package/dist/index.d.ts +38 -1
- package/dist/index.js +212 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2276,6 +2276,17 @@ var ApiClient = class {
|
|
|
2276
2276
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
2277
2277
|
});
|
|
2278
2278
|
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Trade a still-valid token for a fresh one, so an active user is never
|
|
2281
|
+
* signed out mid-use. The backend caps the whole chain by session age, so
|
|
2282
|
+
* this cannot renew forever; a 401 means "sign in again", not "retry".
|
|
2283
|
+
*/
|
|
2284
|
+
async refreshAccessToken(jwt) {
|
|
2285
|
+
return this._request("/auth/refresh", {
|
|
2286
|
+
method: "POST",
|
|
2287
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2279
2290
|
/** Redeem a decrypted unlock token for a session. */
|
|
2280
2291
|
async exchangeUnlockToken(token) {
|
|
2281
2292
|
return this._request("/auth/unlock-token/exchange", {
|
|
@@ -2416,6 +2427,54 @@ var import_browser = require("@simplewebauthn/browser");
|
|
|
2416
2427
|
|
|
2417
2428
|
// src/social/attestation.ts
|
|
2418
2429
|
var import_dcap_qvl = require("@phala/dcap-qvl");
|
|
2430
|
+
|
|
2431
|
+
// src/social/collateralCache.ts
|
|
2432
|
+
var CACHE_PREFIX = "decane:collateral:v1:";
|
|
2433
|
+
var COLLATERAL_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
2434
|
+
function storage() {
|
|
2435
|
+
try {
|
|
2436
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2437
|
+
} catch {
|
|
2438
|
+
return null;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
function keyFor(teeUrl) {
|
|
2442
|
+
return `${CACHE_PREFIX}${teeUrl}`;
|
|
2443
|
+
}
|
|
2444
|
+
function readCollateral(teeUrl) {
|
|
2445
|
+
const store = storage();
|
|
2446
|
+
if (!store) return null;
|
|
2447
|
+
try {
|
|
2448
|
+
const raw = store.getItem(keyFor(teeUrl));
|
|
2449
|
+
if (!raw) return null;
|
|
2450
|
+
const entry = JSON.parse(raw);
|
|
2451
|
+
if (!entry?.at || Date.now() - entry.at > COLLATERAL_TTL_MS) {
|
|
2452
|
+
store.removeItem(keyFor(teeUrl));
|
|
2453
|
+
return null;
|
|
2454
|
+
}
|
|
2455
|
+
return entry.collateral ?? null;
|
|
2456
|
+
} catch {
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
function writeCollateral(teeUrl, collateral) {
|
|
2461
|
+
const store = storage();
|
|
2462
|
+
if (!store) return;
|
|
2463
|
+
try {
|
|
2464
|
+
store.setItem(keyFor(teeUrl), JSON.stringify({ at: Date.now(), collateral }));
|
|
2465
|
+
} catch {
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
function dropCollateral(teeUrl) {
|
|
2469
|
+
const store = storage();
|
|
2470
|
+
if (!store) return;
|
|
2471
|
+
try {
|
|
2472
|
+
store.removeItem(keyFor(teeUrl));
|
|
2473
|
+
} catch {
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
// src/social/attestation.ts
|
|
2419
2478
|
init_errors();
|
|
2420
2479
|
|
|
2421
2480
|
// ../../infra/tee-measurement.json
|
|
@@ -2443,6 +2502,9 @@ function hexToBytes(hex) {
|
|
|
2443
2502
|
function bytesToHex(bytes) {
|
|
2444
2503
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2445
2504
|
}
|
|
2505
|
+
function nowSeconds() {
|
|
2506
|
+
return Math.floor(Date.now() / 1e3);
|
|
2507
|
+
}
|
|
2446
2508
|
async function verifyTeeAttestation(teeUrl, nonce) {
|
|
2447
2509
|
const res = await fetch(`${teeUrl}/attestation`, {
|
|
2448
2510
|
method: "POST",
|
|
@@ -2452,9 +2514,22 @@ async function verifyTeeAttestation(teeUrl, nonce) {
|
|
|
2452
2514
|
if (!res.ok) throw new AttestationError("quote fetch failed");
|
|
2453
2515
|
const { quote, measurement } = await res.json();
|
|
2454
2516
|
let verified;
|
|
2517
|
+
const cached = readCollateral(teeUrl);
|
|
2455
2518
|
try {
|
|
2456
|
-
|
|
2457
|
-
|
|
2519
|
+
if (cached) {
|
|
2520
|
+
try {
|
|
2521
|
+
verified = await (0, import_dcap_qvl.verify)(hexToBytes(quote), cached, nowSeconds());
|
|
2522
|
+
} catch {
|
|
2523
|
+
dropCollateral(teeUrl);
|
|
2524
|
+
verified = await (0, import_dcap_qvl.getCollateralAndVerify)(hexToBytes(quote));
|
|
2525
|
+
}
|
|
2526
|
+
} else {
|
|
2527
|
+
const collateral = await (0, import_dcap_qvl.getCollateral)(import_dcap_qvl.PHALA_PCCS_URL, hexToBytes(quote));
|
|
2528
|
+
verified = await (0, import_dcap_qvl.verify)(hexToBytes(quote), collateral, nowSeconds());
|
|
2529
|
+
writeCollateral(teeUrl, collateral);
|
|
2530
|
+
}
|
|
2531
|
+
} catch (e) {
|
|
2532
|
+
if (e instanceof AttestationError) throw e;
|
|
2458
2533
|
throw new AttestationError("quote signature invalid");
|
|
2459
2534
|
}
|
|
2460
2535
|
if (verified.status === "Revoked") {
|
|
@@ -2496,14 +2571,51 @@ async function teeFetch(url, init) {
|
|
|
2496
2571
|
}
|
|
2497
2572
|
var ATTESTATION_TTL_MS = 10 * 6e4;
|
|
2498
2573
|
var attestationCache = /* @__PURE__ */ new Map();
|
|
2574
|
+
var ATTESTATION_PERSIST_PREFIX = "decane:attested:v1:";
|
|
2575
|
+
function persistedAt(teeUrl) {
|
|
2576
|
+
try {
|
|
2577
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
2578
|
+
const raw = sessionStorage.getItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2579
|
+
const at = raw ? Number(raw) : NaN;
|
|
2580
|
+
return Number.isFinite(at) ? at : null;
|
|
2581
|
+
} catch {
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
function rememberAttested(teeUrl, at) {
|
|
2586
|
+
try {
|
|
2587
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2588
|
+
sessionStorage.setItem(ATTESTATION_PERSIST_PREFIX + teeUrl, String(at));
|
|
2589
|
+
}
|
|
2590
|
+
} catch {
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
function forgetAttested(teeUrl) {
|
|
2594
|
+
try {
|
|
2595
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2596
|
+
sessionStorage.removeItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2597
|
+
}
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2499
2601
|
function attestOnce(teeUrl, verify) {
|
|
2500
2602
|
const hit = attestationCache.get(teeUrl);
|
|
2501
2603
|
if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
|
|
2502
|
-
const
|
|
2604
|
+
const at = persistedAt(teeUrl);
|
|
2605
|
+
if (at && Date.now() - at < ATTESTATION_TTL_MS) {
|
|
2606
|
+
const resolved = Promise.resolve();
|
|
2607
|
+
attestationCache.set(teeUrl, { at, promise: resolved });
|
|
2608
|
+
return resolved;
|
|
2609
|
+
}
|
|
2610
|
+
const startedAt = Date.now();
|
|
2611
|
+
const promise = verify().then(() => {
|
|
2612
|
+
rememberAttested(teeUrl, startedAt);
|
|
2613
|
+
}).catch((e) => {
|
|
2503
2614
|
attestationCache.delete(teeUrl);
|
|
2615
|
+
forgetAttested(teeUrl);
|
|
2504
2616
|
throw e;
|
|
2505
2617
|
});
|
|
2506
|
-
attestationCache.set(teeUrl, { at:
|
|
2618
|
+
attestationCache.set(teeUrl, { at: startedAt, promise });
|
|
2507
2619
|
return promise;
|
|
2508
2620
|
}
|
|
2509
2621
|
function prewarmAttestation(teeUrl) {
|
|
@@ -3775,29 +3887,44 @@ function clearLastUser(key) {
|
|
|
3775
3887
|
function sessionPersistKey(apiKey) {
|
|
3776
3888
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3777
3889
|
}
|
|
3890
|
+
var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3891
|
+
function useCrossTabSessionStore(enabled) {
|
|
3892
|
+
sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3893
|
+
}
|
|
3778
3894
|
function loadSession(key) {
|
|
3779
3895
|
try {
|
|
3780
|
-
const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
3781
|
-
|
|
3896
|
+
const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
|
|
3897
|
+
if (!raw) return null;
|
|
3898
|
+
const parsed = JSON.parse(raw);
|
|
3899
|
+
if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
|
|
3900
|
+
clearSessionPersist(key);
|
|
3901
|
+
return null;
|
|
3902
|
+
}
|
|
3903
|
+
return parsed;
|
|
3782
3904
|
} catch {
|
|
3783
3905
|
return null;
|
|
3784
3906
|
}
|
|
3785
3907
|
}
|
|
3786
3908
|
function saveSession(key, s) {
|
|
3787
3909
|
try {
|
|
3788
|
-
|
|
3910
|
+
sessionStore()?.setItem(key, JSON.stringify(s));
|
|
3789
3911
|
} catch {
|
|
3790
3912
|
}
|
|
3791
3913
|
}
|
|
3792
3914
|
function clearSessionPersist(key) {
|
|
3793
3915
|
try {
|
|
3794
|
-
sessionStorage.removeItem(key);
|
|
3916
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
|
|
3917
|
+
} catch {
|
|
3918
|
+
}
|
|
3919
|
+
try {
|
|
3920
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
3795
3921
|
} catch {
|
|
3796
3922
|
}
|
|
3797
3923
|
}
|
|
3798
3924
|
async function createDecaneConnect(config) {
|
|
3799
3925
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3800
3926
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3927
|
+
useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
|
|
3801
3928
|
if (!config.chains?.length)
|
|
3802
3929
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3803
3930
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3816,7 +3943,7 @@ async function createDecaneConnect(config) {
|
|
|
3816
3943
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3817
3944
|
const events = new EventEmitter();
|
|
3818
3945
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
3819
|
-
const
|
|
3946
|
+
const storage2 = new Storage();
|
|
3820
3947
|
const session = new SessionManager(
|
|
3821
3948
|
events,
|
|
3822
3949
|
durationMs,
|
|
@@ -3914,6 +4041,7 @@ async function createDecaneConnect(config) {
|
|
|
3914
4041
|
clearSessionPersist(SESSION_KEY);
|
|
3915
4042
|
}
|
|
3916
4043
|
let accessTokenExpiry = null;
|
|
4044
|
+
let tokenRefresh = null;
|
|
3917
4045
|
function scheduleAccessTokenExpiry(jwt, expiresAt) {
|
|
3918
4046
|
if (accessTokenExpiry) clearTimeout(accessTokenExpiry);
|
|
3919
4047
|
accessTokenExpiry = setTimeout(() => {
|
|
@@ -3923,6 +4051,40 @@ async function createDecaneConnect(config) {
|
|
|
3923
4051
|
dropLiveSession();
|
|
3924
4052
|
events.emit("session-expired");
|
|
3925
4053
|
}, Math.max(0, expiresAt - Date.now()));
|
|
4054
|
+
scheduleTokenRefresh(jwt, expiresAt);
|
|
4055
|
+
}
|
|
4056
|
+
function scheduleTokenRefresh(jwt, expiresAt) {
|
|
4057
|
+
if (tokenRefresh) clearTimeout(tokenRefresh);
|
|
4058
|
+
const lifetime = expiresAt - Date.now();
|
|
4059
|
+
if (lifetime <= 0) return;
|
|
4060
|
+
const at = Math.max(5e3, lifetime * 0.75);
|
|
4061
|
+
tokenRefresh = setTimeout(() => {
|
|
4062
|
+
tokenRefresh = null;
|
|
4063
|
+
void renewAccessToken(jwt);
|
|
4064
|
+
}, at);
|
|
4065
|
+
}
|
|
4066
|
+
async function renewAccessToken(expected) {
|
|
4067
|
+
if (auth?.jwt !== expected) return;
|
|
4068
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
4069
|
+
const onVisible = () => {
|
|
4070
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
4071
|
+
if (auth?.jwt === expected) void renewAccessToken(expected);
|
|
4072
|
+
};
|
|
4073
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
try {
|
|
4077
|
+
const { jwt: fresh } = await api.refreshAccessToken(expected);
|
|
4078
|
+
if (auth?.jwt !== expected) return;
|
|
4079
|
+
auth = { ...auth, jwt: fresh };
|
|
4080
|
+
const freshExpiry = decodeAccessToken(fresh)?.expiresAt;
|
|
4081
|
+
if (freshExpiry) {
|
|
4082
|
+
scheduleAccessTokenExpiry(fresh, freshExpiry);
|
|
4083
|
+
persistLiveSession();
|
|
4084
|
+
}
|
|
4085
|
+
events.emit("session-renewed");
|
|
4086
|
+
} catch {
|
|
4087
|
+
}
|
|
3926
4088
|
}
|
|
3927
4089
|
if (persistSession) {
|
|
3928
4090
|
const saved = loadSession(SESSION_KEY) ?? (LEGACY_SESSION_KEY !== SESSION_KEY ? loadSession(LEGACY_SESSION_KEY) : null);
|
|
@@ -3959,7 +4121,7 @@ async function createDecaneConnect(config) {
|
|
|
3959
4121
|
}
|
|
3960
4122
|
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3961
4123
|
try {
|
|
3962
|
-
const stored = await
|
|
4124
|
+
const stored = await storage2.getShare(userId);
|
|
3963
4125
|
if (!stored) return;
|
|
3964
4126
|
const { token } = await api.issueUnlockToken(jwt);
|
|
3965
4127
|
const unlockTokenIv = generateRandomBytes(12);
|
|
@@ -3968,13 +4130,13 @@ async function createDecaneConnect(config) {
|
|
|
3968
4130
|
new TextEncoder().encode(token),
|
|
3969
4131
|
unlockTokenIv
|
|
3970
4132
|
);
|
|
3971
|
-
await
|
|
4133
|
+
await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3972
4134
|
} catch (e) {
|
|
3973
4135
|
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3974
4136
|
}
|
|
3975
4137
|
}
|
|
3976
4138
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3977
|
-
const stored = await
|
|
4139
|
+
const stored = await storage2.getShare(userId);
|
|
3978
4140
|
if (!stored) {
|
|
3979
4141
|
throw new CallSequenceError(
|
|
3980
4142
|
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
@@ -3983,21 +4145,21 @@ async function createDecaneConnect(config) {
|
|
|
3983
4145
|
const wrappingKey = await getWrappingKey(stored);
|
|
3984
4146
|
const iv = generateRandomBytes(12);
|
|
3985
4147
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3986
|
-
await
|
|
4148
|
+
await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3987
4149
|
}
|
|
3988
4150
|
function hasPasswordUnlock(stored) {
|
|
3989
4151
|
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3990
4152
|
}
|
|
3991
4153
|
async function passwordDeviceCandidate() {
|
|
3992
4154
|
if (persisted?.userId) {
|
|
3993
|
-
const stored = await
|
|
4155
|
+
const stored = await storage2.getShare(persisted.userId);
|
|
3994
4156
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3995
4157
|
}
|
|
3996
4158
|
if (lastUserId) {
|
|
3997
|
-
const stored = await
|
|
4159
|
+
const stored = await storage2.getShare(lastUserId);
|
|
3998
4160
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3999
4161
|
}
|
|
4000
|
-
const candidates = (await
|
|
4162
|
+
const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
|
|
4001
4163
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4002
4164
|
}
|
|
4003
4165
|
async function getWrappingKey(stored) {
|
|
@@ -4025,7 +4187,7 @@ async function createDecaneConnect(config) {
|
|
|
4025
4187
|
let credentialId;
|
|
4026
4188
|
let prfOutput;
|
|
4027
4189
|
try {
|
|
4028
|
-
const label = (await
|
|
4190
|
+
const label = (await storage2.getShare(userId))?.profile?.email;
|
|
4029
4191
|
({ credentialId, prfOutput } = await registerWebAuthnCredential({
|
|
4030
4192
|
api,
|
|
4031
4193
|
jwt,
|
|
@@ -4057,8 +4219,8 @@ async function createDecaneConnect(config) {
|
|
|
4057
4219
|
const wrappingKey = await importAesKey(prfOutput);
|
|
4058
4220
|
const iv = generateRandomBytes(12);
|
|
4059
4221
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4060
|
-
const existing = await
|
|
4061
|
-
await
|
|
4222
|
+
const existing = await storage2.getShare(userId);
|
|
4223
|
+
await storage2.putShare({
|
|
4062
4224
|
...existing,
|
|
4063
4225
|
userId,
|
|
4064
4226
|
wrappedDeviceShare,
|
|
@@ -4142,7 +4304,7 @@ async function createDecaneConnect(config) {
|
|
|
4142
4304
|
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
4143
4305
|
const iv = generateRandomBytes(12);
|
|
4144
4306
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4145
|
-
await
|
|
4307
|
+
await storage2.putShare({
|
|
4146
4308
|
userId,
|
|
4147
4309
|
wrappedDeviceShare,
|
|
4148
4310
|
iv,
|
|
@@ -4216,7 +4378,7 @@ async function createDecaneConnect(config) {
|
|
|
4216
4378
|
shareSet.device,
|
|
4217
4379
|
iv
|
|
4218
4380
|
);
|
|
4219
|
-
await
|
|
4381
|
+
await storage2.putShare({
|
|
4220
4382
|
userId,
|
|
4221
4383
|
wrappedDeviceShare,
|
|
4222
4384
|
iv,
|
|
@@ -4320,7 +4482,7 @@ async function createDecaneConnect(config) {
|
|
|
4320
4482
|
async function returningUserUnlock(userId, jwt) {
|
|
4321
4483
|
const rawAddresses = await api.getAddresses(jwt);
|
|
4322
4484
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|
|
4323
|
-
const stored = await
|
|
4485
|
+
const stored = await storage2.getShare(userId);
|
|
4324
4486
|
let deviceShare;
|
|
4325
4487
|
let joinedViaPasskeySync = false;
|
|
4326
4488
|
if (!stored) {
|
|
@@ -4359,7 +4521,7 @@ async function createDecaneConnect(config) {
|
|
|
4359
4521
|
rpId,
|
|
4360
4522
|
teeUrl,
|
|
4361
4523
|
api,
|
|
4362
|
-
storage,
|
|
4524
|
+
storage: storage2,
|
|
4363
4525
|
onProgress: config.onRecoveryProgress,
|
|
4364
4526
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
4365
4527
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
@@ -4373,7 +4535,7 @@ async function createDecaneConnect(config) {
|
|
|
4373
4535
|
});
|
|
4374
4536
|
deviceShare = recovered.deviceShare;
|
|
4375
4537
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4376
|
-
if (!deferDeviceProtection && !await
|
|
4538
|
+
if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4377
4539
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4378
4540
|
}
|
|
4379
4541
|
} else {
|
|
@@ -4394,7 +4556,7 @@ async function createDecaneConnect(config) {
|
|
|
4394
4556
|
} catch (e) {
|
|
4395
4557
|
if (e instanceof StaleDeviceShareError) {
|
|
4396
4558
|
zeroize(deviceShare);
|
|
4397
|
-
await
|
|
4559
|
+
await storage2.deleteShare(userId);
|
|
4398
4560
|
if (stored) return returningUserUnlock(userId, jwt);
|
|
4399
4561
|
} else {
|
|
4400
4562
|
zeroize(deviceShare);
|
|
@@ -4418,7 +4580,7 @@ async function createDecaneConnect(config) {
|
|
|
4418
4580
|
const result = await tee.rotateShares(jwt);
|
|
4419
4581
|
try {
|
|
4420
4582
|
await persistRotatedDeviceShare(userId, result.deviceShare);
|
|
4421
|
-
const refreshed = await
|
|
4583
|
+
const refreshed = await storage2.getShare(userId);
|
|
4422
4584
|
if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
|
|
4423
4585
|
await api.putRecoveryBackup(jwt, {
|
|
4424
4586
|
wrappedDeviceShare: refreshed.wrappedDeviceShare,
|
|
@@ -4451,13 +4613,13 @@ async function createDecaneConnect(config) {
|
|
|
4451
4613
|
}
|
|
4452
4614
|
async function passkeyLoginCandidate() {
|
|
4453
4615
|
if (!persisted?.userId || !isPasskeySupported()) return null;
|
|
4454
|
-
return usablePasskeyShare(await
|
|
4616
|
+
return usablePasskeyShare(await storage2.getShare(persisted.userId));
|
|
4455
4617
|
}
|
|
4456
4618
|
async function passkeyDeviceCandidate() {
|
|
4457
4619
|
if (!isPasskeySupported()) return null;
|
|
4458
4620
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4459
|
-
if (lastUserId) return usablePasskeyShare(await
|
|
4460
|
-
const candidates = (await
|
|
4621
|
+
if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
|
|
4622
|
+
const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4461
4623
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4462
4624
|
}
|
|
4463
4625
|
async function canOfferPasskey() {
|
|
@@ -4465,15 +4627,15 @@ async function createDecaneConnect(config) {
|
|
|
4465
4627
|
if (await passkeyDeviceCandidate()) return true;
|
|
4466
4628
|
const known = persisted?.userId ?? lastUserId;
|
|
4467
4629
|
if (!known) return false;
|
|
4468
|
-
return await
|
|
4630
|
+
return await storage2.getShare(known) == null;
|
|
4469
4631
|
}
|
|
4470
4632
|
async function rememberProfile(userId, profile) {
|
|
4471
4633
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4472
4634
|
if (!Object.keys(incoming).length) return;
|
|
4473
4635
|
try {
|
|
4474
|
-
const stored = await
|
|
4636
|
+
const stored = await storage2.getShare(userId);
|
|
4475
4637
|
if (!stored) return;
|
|
4476
|
-
await
|
|
4638
|
+
await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4477
4639
|
} catch {
|
|
4478
4640
|
}
|
|
4479
4641
|
}
|
|
@@ -4497,7 +4659,7 @@ async function createDecaneConnect(config) {
|
|
|
4497
4659
|
savePersisted(PERSIST_KEY, persisted);
|
|
4498
4660
|
events.emit("connected");
|
|
4499
4661
|
persistLiveSession();
|
|
4500
|
-
const stored = await
|
|
4662
|
+
const stored = await storage2.getShare(userId);
|
|
4501
4663
|
return {
|
|
4502
4664
|
addresses,
|
|
4503
4665
|
isNewUser: false,
|
|
@@ -4670,7 +4832,7 @@ async function createDecaneConnect(config) {
|
|
|
4670
4832
|
events.emit("connected");
|
|
4671
4833
|
persistLiveSession();
|
|
4672
4834
|
scheduleAccessTokenExpiry(token, claims.expiresAt);
|
|
4673
|
-
const stored = await
|
|
4835
|
+
const stored = await storage2.getShare(claims.userId);
|
|
4674
4836
|
return {
|
|
4675
4837
|
addresses,
|
|
4676
4838
|
isNewUser: false,
|
|
@@ -4700,7 +4862,7 @@ async function createDecaneConnect(config) {
|
|
|
4700
4862
|
const candidate = await passkeyDeviceCandidate();
|
|
4701
4863
|
if (candidate) return passkeyLogin(candidate);
|
|
4702
4864
|
const known = persisted?.userId ?? lastUserId;
|
|
4703
|
-
if (isPasskeySupported() && known && await
|
|
4865
|
+
if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
|
|
4704
4866
|
return passkeyLoginWithoutShare(known);
|
|
4705
4867
|
}
|
|
4706
4868
|
throw new NewDeviceError();
|
|
@@ -4719,7 +4881,7 @@ async function createDecaneConnect(config) {
|
|
|
4719
4881
|
async setUnlockPassword(password) {
|
|
4720
4882
|
if (!auth) throw new ReconnectRequiredError();
|
|
4721
4883
|
validateUnlockPassword(password);
|
|
4722
|
-
const stored = await
|
|
4884
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4723
4885
|
if (!stored) throw new NewDeviceError();
|
|
4724
4886
|
const deviceShare = await aesGcmDecrypt(
|
|
4725
4887
|
await getWrappingKey(stored),
|
|
@@ -4738,7 +4900,7 @@ async function createDecaneConnect(config) {
|
|
|
4738
4900
|
);
|
|
4739
4901
|
const iv = generateRandomBytes(12);
|
|
4740
4902
|
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4741
|
-
await
|
|
4903
|
+
await storage2.putShare({
|
|
4742
4904
|
...stored,
|
|
4743
4905
|
unlockMethod: "password",
|
|
4744
4906
|
wrappedDeviceShare,
|
|
@@ -4818,9 +4980,9 @@ async function createDecaneConnect(config) {
|
|
|
4818
4980
|
async removeUnlockPassword() {
|
|
4819
4981
|
if (!auth) throw new ReconnectRequiredError();
|
|
4820
4982
|
await api.revokeUnlockTokens(auth.jwt);
|
|
4821
|
-
const stored = await
|
|
4983
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4822
4984
|
if (!stored || stored.unlockMethod !== "password") return;
|
|
4823
|
-
await
|
|
4985
|
+
await storage2.putShare({
|
|
4824
4986
|
...stored,
|
|
4825
4987
|
wrappedUnlockToken: void 0,
|
|
4826
4988
|
unlockTokenIv: void 0
|
|
@@ -4828,13 +4990,13 @@ async function createDecaneConnect(config) {
|
|
|
4828
4990
|
},
|
|
4829
4991
|
async hasPasskey() {
|
|
4830
4992
|
if (!auth) return false;
|
|
4831
|
-
const stored = await
|
|
4993
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4832
4994
|
return stored?.unlockMethod === "passkey";
|
|
4833
4995
|
},
|
|
4834
4996
|
async addPasskey() {
|
|
4835
4997
|
if (!auth) throw new ReconnectRequiredError();
|
|
4836
4998
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4837
|
-
const stored = await
|
|
4999
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4838
5000
|
if (stored?.unlockMethod === "passkey") return;
|
|
4839
5001
|
if (!stored) {
|
|
4840
5002
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4867,11 +5029,11 @@ async function createDecaneConnect(config) {
|
|
|
4867
5029
|
},
|
|
4868
5030
|
async deviceProtected() {
|
|
4869
5031
|
if (!auth) return false;
|
|
4870
|
-
return await
|
|
5032
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4871
5033
|
},
|
|
4872
5034
|
async protectDevice() {
|
|
4873
5035
|
if (!auth) throw new ReconnectRequiredError();
|
|
4874
|
-
if (await
|
|
5036
|
+
if (await storage2.getShare(auth.userId)) return true;
|
|
4875
5037
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4876
5038
|
const tee = new TeeClient(teeUrl);
|
|
4877
5039
|
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
@@ -4881,7 +5043,7 @@ async function createDecaneConnect(config) {
|
|
|
4881
5043
|
} finally {
|
|
4882
5044
|
zeroize(provisioned);
|
|
4883
5045
|
}
|
|
4884
|
-
return await
|
|
5046
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4885
5047
|
},
|
|
4886
5048
|
async unlock() {
|
|
4887
5049
|
if (!auth) {
|