decane-connect-kit 2.24.1 → 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 +160 -44
- 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 +166 -45
- 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) {
|
|
@@ -3692,29 +3798,44 @@ function clearLastUser(key) {
|
|
|
3692
3798
|
function sessionPersistKey(apiKey) {
|
|
3693
3799
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3694
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
|
+
}
|
|
3695
3805
|
function loadSession(key) {
|
|
3696
3806
|
try {
|
|
3697
|
-
const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
3698
|
-
|
|
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;
|
|
3699
3815
|
} catch {
|
|
3700
3816
|
return null;
|
|
3701
3817
|
}
|
|
3702
3818
|
}
|
|
3703
3819
|
function saveSession(key, s) {
|
|
3704
3820
|
try {
|
|
3705
|
-
|
|
3821
|
+
sessionStore()?.setItem(key, JSON.stringify(s));
|
|
3706
3822
|
} catch {
|
|
3707
3823
|
}
|
|
3708
3824
|
}
|
|
3709
3825
|
function clearSessionPersist(key) {
|
|
3710
3826
|
try {
|
|
3711
|
-
sessionStorage.removeItem(key);
|
|
3827
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
|
|
3828
|
+
} catch {
|
|
3829
|
+
}
|
|
3830
|
+
try {
|
|
3831
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
3712
3832
|
} catch {
|
|
3713
3833
|
}
|
|
3714
3834
|
}
|
|
3715
3835
|
async function createDecaneConnect(config) {
|
|
3716
3836
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3717
3837
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3838
|
+
useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
|
|
3718
3839
|
if (!config.chains?.length)
|
|
3719
3840
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3720
3841
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3733,7 +3854,7 @@ async function createDecaneConnect(config) {
|
|
|
3733
3854
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3734
3855
|
const events = new EventEmitter();
|
|
3735
3856
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
3736
|
-
const
|
|
3857
|
+
const storage2 = new Storage();
|
|
3737
3858
|
const session = new SessionManager(
|
|
3738
3859
|
events,
|
|
3739
3860
|
durationMs,
|
|
@@ -3876,7 +3997,7 @@ async function createDecaneConnect(config) {
|
|
|
3876
3997
|
}
|
|
3877
3998
|
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3878
3999
|
try {
|
|
3879
|
-
const stored = await
|
|
4000
|
+
const stored = await storage2.getShare(userId);
|
|
3880
4001
|
if (!stored) return;
|
|
3881
4002
|
const { token } = await api.issueUnlockToken(jwt);
|
|
3882
4003
|
const unlockTokenIv = generateRandomBytes(12);
|
|
@@ -3885,13 +4006,13 @@ async function createDecaneConnect(config) {
|
|
|
3885
4006
|
new TextEncoder().encode(token),
|
|
3886
4007
|
unlockTokenIv
|
|
3887
4008
|
);
|
|
3888
|
-
await
|
|
4009
|
+
await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3889
4010
|
} catch (e) {
|
|
3890
4011
|
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3891
4012
|
}
|
|
3892
4013
|
}
|
|
3893
4014
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3894
|
-
const stored = await
|
|
4015
|
+
const stored = await storage2.getShare(userId);
|
|
3895
4016
|
if (!stored) {
|
|
3896
4017
|
throw new CallSequenceError(
|
|
3897
4018
|
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
@@ -3900,21 +4021,21 @@ async function createDecaneConnect(config) {
|
|
|
3900
4021
|
const wrappingKey = await getWrappingKey(stored);
|
|
3901
4022
|
const iv = generateRandomBytes(12);
|
|
3902
4023
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3903
|
-
await
|
|
4024
|
+
await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3904
4025
|
}
|
|
3905
4026
|
function hasPasswordUnlock(stored) {
|
|
3906
4027
|
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3907
4028
|
}
|
|
3908
4029
|
async function passwordDeviceCandidate() {
|
|
3909
4030
|
if (persisted?.userId) {
|
|
3910
|
-
const stored = await
|
|
4031
|
+
const stored = await storage2.getShare(persisted.userId);
|
|
3911
4032
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3912
4033
|
}
|
|
3913
4034
|
if (lastUserId) {
|
|
3914
|
-
const stored = await
|
|
4035
|
+
const stored = await storage2.getShare(lastUserId);
|
|
3915
4036
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3916
4037
|
}
|
|
3917
|
-
const candidates = (await
|
|
4038
|
+
const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
|
|
3918
4039
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3919
4040
|
}
|
|
3920
4041
|
async function getWrappingKey(stored) {
|
|
@@ -3942,7 +4063,7 @@ async function createDecaneConnect(config) {
|
|
|
3942
4063
|
let credentialId;
|
|
3943
4064
|
let prfOutput;
|
|
3944
4065
|
try {
|
|
3945
|
-
const label = (await
|
|
4066
|
+
const label = (await storage2.getShare(userId))?.profile?.email;
|
|
3946
4067
|
({ credentialId, prfOutput } = await registerWebAuthnCredential({
|
|
3947
4068
|
api,
|
|
3948
4069
|
jwt,
|
|
@@ -3974,8 +4095,8 @@ async function createDecaneConnect(config) {
|
|
|
3974
4095
|
const wrappingKey = await importAesKey(prfOutput);
|
|
3975
4096
|
const iv = generateRandomBytes(12);
|
|
3976
4097
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3977
|
-
const existing = await
|
|
3978
|
-
await
|
|
4098
|
+
const existing = await storage2.getShare(userId);
|
|
4099
|
+
await storage2.putShare({
|
|
3979
4100
|
...existing,
|
|
3980
4101
|
userId,
|
|
3981
4102
|
wrappedDeviceShare,
|
|
@@ -4059,7 +4180,7 @@ async function createDecaneConnect(config) {
|
|
|
4059
4180
|
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
4060
4181
|
const iv = generateRandomBytes(12);
|
|
4061
4182
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4062
|
-
await
|
|
4183
|
+
await storage2.putShare({
|
|
4063
4184
|
userId,
|
|
4064
4185
|
wrappedDeviceShare,
|
|
4065
4186
|
iv,
|
|
@@ -4133,7 +4254,7 @@ async function createDecaneConnect(config) {
|
|
|
4133
4254
|
shareSet.device,
|
|
4134
4255
|
iv
|
|
4135
4256
|
);
|
|
4136
|
-
await
|
|
4257
|
+
await storage2.putShare({
|
|
4137
4258
|
userId,
|
|
4138
4259
|
wrappedDeviceShare,
|
|
4139
4260
|
iv,
|
|
@@ -4237,7 +4358,7 @@ async function createDecaneConnect(config) {
|
|
|
4237
4358
|
async function returningUserUnlock(userId, jwt) {
|
|
4238
4359
|
const rawAddresses = await api.getAddresses(jwt);
|
|
4239
4360
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|
|
4240
|
-
const stored = await
|
|
4361
|
+
const stored = await storage2.getShare(userId);
|
|
4241
4362
|
let deviceShare;
|
|
4242
4363
|
let joinedViaPasskeySync = false;
|
|
4243
4364
|
if (!stored) {
|
|
@@ -4276,7 +4397,7 @@ async function createDecaneConnect(config) {
|
|
|
4276
4397
|
rpId,
|
|
4277
4398
|
teeUrl,
|
|
4278
4399
|
api,
|
|
4279
|
-
storage,
|
|
4400
|
+
storage: storage2,
|
|
4280
4401
|
onProgress: config.onRecoveryProgress,
|
|
4281
4402
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
4282
4403
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
@@ -4290,7 +4411,7 @@ async function createDecaneConnect(config) {
|
|
|
4290
4411
|
});
|
|
4291
4412
|
deviceShare = recovered.deviceShare;
|
|
4292
4413
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4293
|
-
if (!deferDeviceProtection && !await
|
|
4414
|
+
if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4294
4415
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4295
4416
|
}
|
|
4296
4417
|
} else {
|
|
@@ -4311,7 +4432,7 @@ async function createDecaneConnect(config) {
|
|
|
4311
4432
|
} catch (e) {
|
|
4312
4433
|
if (e instanceof StaleDeviceShareError) {
|
|
4313
4434
|
zeroize(deviceShare);
|
|
4314
|
-
await
|
|
4435
|
+
await storage2.deleteShare(userId);
|
|
4315
4436
|
if (stored) return returningUserUnlock(userId, jwt);
|
|
4316
4437
|
} else {
|
|
4317
4438
|
zeroize(deviceShare);
|
|
@@ -4335,7 +4456,7 @@ async function createDecaneConnect(config) {
|
|
|
4335
4456
|
const result = await tee.rotateShares(jwt);
|
|
4336
4457
|
try {
|
|
4337
4458
|
await persistRotatedDeviceShare(userId, result.deviceShare);
|
|
4338
|
-
const refreshed = await
|
|
4459
|
+
const refreshed = await storage2.getShare(userId);
|
|
4339
4460
|
if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
|
|
4340
4461
|
await api.putRecoveryBackup(jwt, {
|
|
4341
4462
|
wrappedDeviceShare: refreshed.wrappedDeviceShare,
|
|
@@ -4368,13 +4489,13 @@ async function createDecaneConnect(config) {
|
|
|
4368
4489
|
}
|
|
4369
4490
|
async function passkeyLoginCandidate() {
|
|
4370
4491
|
if (!persisted?.userId || !isPasskeySupported()) return null;
|
|
4371
|
-
return usablePasskeyShare(await
|
|
4492
|
+
return usablePasskeyShare(await storage2.getShare(persisted.userId));
|
|
4372
4493
|
}
|
|
4373
4494
|
async function passkeyDeviceCandidate() {
|
|
4374
4495
|
if (!isPasskeySupported()) return null;
|
|
4375
4496
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4376
|
-
if (lastUserId) return usablePasskeyShare(await
|
|
4377
|
-
const candidates = (await
|
|
4497
|
+
if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
|
|
4498
|
+
const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4378
4499
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4379
4500
|
}
|
|
4380
4501
|
async function canOfferPasskey() {
|
|
@@ -4382,15 +4503,15 @@ async function createDecaneConnect(config) {
|
|
|
4382
4503
|
if (await passkeyDeviceCandidate()) return true;
|
|
4383
4504
|
const known = persisted?.userId ?? lastUserId;
|
|
4384
4505
|
if (!known) return false;
|
|
4385
|
-
return await
|
|
4506
|
+
return await storage2.getShare(known) == null;
|
|
4386
4507
|
}
|
|
4387
4508
|
async function rememberProfile(userId, profile) {
|
|
4388
4509
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4389
4510
|
if (!Object.keys(incoming).length) return;
|
|
4390
4511
|
try {
|
|
4391
|
-
const stored = await
|
|
4512
|
+
const stored = await storage2.getShare(userId);
|
|
4392
4513
|
if (!stored) return;
|
|
4393
|
-
await
|
|
4514
|
+
await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4394
4515
|
} catch {
|
|
4395
4516
|
}
|
|
4396
4517
|
}
|
|
@@ -4414,7 +4535,7 @@ async function createDecaneConnect(config) {
|
|
|
4414
4535
|
savePersisted(PERSIST_KEY, persisted);
|
|
4415
4536
|
events.emit("connected");
|
|
4416
4537
|
persistLiveSession();
|
|
4417
|
-
const stored = await
|
|
4538
|
+
const stored = await storage2.getShare(userId);
|
|
4418
4539
|
return {
|
|
4419
4540
|
addresses,
|
|
4420
4541
|
isNewUser: false,
|
|
@@ -4587,7 +4708,7 @@ async function createDecaneConnect(config) {
|
|
|
4587
4708
|
events.emit("connected");
|
|
4588
4709
|
persistLiveSession();
|
|
4589
4710
|
scheduleAccessTokenExpiry(token, claims.expiresAt);
|
|
4590
|
-
const stored = await
|
|
4711
|
+
const stored = await storage2.getShare(claims.userId);
|
|
4591
4712
|
return {
|
|
4592
4713
|
addresses,
|
|
4593
4714
|
isNewUser: false,
|
|
@@ -4617,7 +4738,7 @@ async function createDecaneConnect(config) {
|
|
|
4617
4738
|
const candidate = await passkeyDeviceCandidate();
|
|
4618
4739
|
if (candidate) return passkeyLogin(candidate);
|
|
4619
4740
|
const known = persisted?.userId ?? lastUserId;
|
|
4620
|
-
if (isPasskeySupported() && known && await
|
|
4741
|
+
if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
|
|
4621
4742
|
return passkeyLoginWithoutShare(known);
|
|
4622
4743
|
}
|
|
4623
4744
|
throw new NewDeviceError();
|
|
@@ -4636,7 +4757,7 @@ async function createDecaneConnect(config) {
|
|
|
4636
4757
|
async setUnlockPassword(password) {
|
|
4637
4758
|
if (!auth) throw new ReconnectRequiredError();
|
|
4638
4759
|
validateUnlockPassword(password);
|
|
4639
|
-
const stored = await
|
|
4760
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4640
4761
|
if (!stored) throw new NewDeviceError();
|
|
4641
4762
|
const deviceShare = await aesGcmDecrypt(
|
|
4642
4763
|
await getWrappingKey(stored),
|
|
@@ -4655,7 +4776,7 @@ async function createDecaneConnect(config) {
|
|
|
4655
4776
|
);
|
|
4656
4777
|
const iv = generateRandomBytes(12);
|
|
4657
4778
|
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4658
|
-
await
|
|
4779
|
+
await storage2.putShare({
|
|
4659
4780
|
...stored,
|
|
4660
4781
|
unlockMethod: "password",
|
|
4661
4782
|
wrappedDeviceShare,
|
|
@@ -4735,9 +4856,9 @@ async function createDecaneConnect(config) {
|
|
|
4735
4856
|
async removeUnlockPassword() {
|
|
4736
4857
|
if (!auth) throw new ReconnectRequiredError();
|
|
4737
4858
|
await api.revokeUnlockTokens(auth.jwt);
|
|
4738
|
-
const stored = await
|
|
4859
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4739
4860
|
if (!stored || stored.unlockMethod !== "password") return;
|
|
4740
|
-
await
|
|
4861
|
+
await storage2.putShare({
|
|
4741
4862
|
...stored,
|
|
4742
4863
|
wrappedUnlockToken: void 0,
|
|
4743
4864
|
unlockTokenIv: void 0
|
|
@@ -4745,13 +4866,13 @@ async function createDecaneConnect(config) {
|
|
|
4745
4866
|
},
|
|
4746
4867
|
async hasPasskey() {
|
|
4747
4868
|
if (!auth) return false;
|
|
4748
|
-
const stored = await
|
|
4869
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4749
4870
|
return stored?.unlockMethod === "passkey";
|
|
4750
4871
|
},
|
|
4751
4872
|
async addPasskey() {
|
|
4752
4873
|
if (!auth) throw new ReconnectRequiredError();
|
|
4753
4874
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4754
|
-
const stored = await
|
|
4875
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4755
4876
|
if (stored?.unlockMethod === "passkey") return;
|
|
4756
4877
|
if (!stored) {
|
|
4757
4878
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4784,11 +4905,11 @@ async function createDecaneConnect(config) {
|
|
|
4784
4905
|
},
|
|
4785
4906
|
async deviceProtected() {
|
|
4786
4907
|
if (!auth) return false;
|
|
4787
|
-
return await
|
|
4908
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4788
4909
|
},
|
|
4789
4910
|
async protectDevice() {
|
|
4790
4911
|
if (!auth) throw new ReconnectRequiredError();
|
|
4791
|
-
if (await
|
|
4912
|
+
if (await storage2.getShare(auth.userId)) return true;
|
|
4792
4913
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4793
4914
|
const tee = new TeeClient(teeUrl);
|
|
4794
4915
|
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
@@ -4798,7 +4919,7 @@ async function createDecaneConnect(config) {
|
|
|
4798
4919
|
} finally {
|
|
4799
4920
|
zeroize(provisioned);
|
|
4800
4921
|
}
|
|
4801
|
-
return await
|
|
4922
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4802
4923
|
},
|
|
4803
4924
|
async unlock() {
|
|
4804
4925
|
if (!auth) {
|