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.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`.
|
|
@@ -287,7 +308,13 @@ interface SignTypedDataRequest {
|
|
|
287
308
|
message: Record<string, unknown>;
|
|
288
309
|
primaryType?: string;
|
|
289
310
|
}
|
|
290
|
-
type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
|
|
311
|
+
type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
|
|
312
|
+
/**
|
|
313
|
+
* The access token was renewed while the user was active, so the session
|
|
314
|
+
* continued instead of ending. Informational: apps that cache the token need
|
|
315
|
+
* to re-read it, most do not.
|
|
316
|
+
*/
|
|
317
|
+
| 'session-renewed' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
|
|
291
318
|
/**
|
|
292
319
|
* Emitted when this wallet could not be enrolled for server-assisted
|
|
293
320
|
* recovery. Not fatal — the wallet works — but until it is enrolled the user
|
|
@@ -716,6 +743,16 @@ declare class ApiClient {
|
|
|
716
743
|
token: string;
|
|
717
744
|
expiresInDays: number;
|
|
718
745
|
}>;
|
|
746
|
+
/**
|
|
747
|
+
* Trade a still-valid token for a fresh one, so an active user is never
|
|
748
|
+
* signed out mid-use. The backend caps the whole chain by session age, so
|
|
749
|
+
* this cannot renew forever; a 401 means "sign in again", not "retry".
|
|
750
|
+
*/
|
|
751
|
+
refreshAccessToken(jwt: string): Promise<{
|
|
752
|
+
jwt: string;
|
|
753
|
+
expiresIn: number;
|
|
754
|
+
sessionExpiresIn: number;
|
|
755
|
+
}>;
|
|
719
756
|
/** Redeem a decrypted unlock token for a session. */
|
|
720
757
|
exchangeUnlockToken(token: string): Promise<{
|
|
721
758
|
jwt: string;
|
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`.
|
|
@@ -287,7 +308,13 @@ interface SignTypedDataRequest {
|
|
|
287
308
|
message: Record<string, unknown>;
|
|
288
309
|
primaryType?: string;
|
|
289
310
|
}
|
|
290
|
-
type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
|
|
311
|
+
type SdkEvent = 'connected' | 'disconnected' | 'session-expired'
|
|
312
|
+
/**
|
|
313
|
+
* The access token was renewed while the user was active, so the session
|
|
314
|
+
* continued instead of ending. Informational: apps that cache the token need
|
|
315
|
+
* to re-read it, most do not.
|
|
316
|
+
*/
|
|
317
|
+
| 'session-renewed' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
|
|
291
318
|
/**
|
|
292
319
|
* Emitted when this wallet could not be enrolled for server-assisted
|
|
293
320
|
* recovery. Not fatal — the wallet works — but until it is enrolled the user
|
|
@@ -716,6 +743,16 @@ declare class ApiClient {
|
|
|
716
743
|
token: string;
|
|
717
744
|
expiresInDays: number;
|
|
718
745
|
}>;
|
|
746
|
+
/**
|
|
747
|
+
* Trade a still-valid token for a fresh one, so an active user is never
|
|
748
|
+
* signed out mid-use. The backend caps the whole chain by session age, so
|
|
749
|
+
* this cannot renew forever; a 401 means "sign in again", not "retry".
|
|
750
|
+
*/
|
|
751
|
+
refreshAccessToken(jwt: string): Promise<{
|
|
752
|
+
jwt: string;
|
|
753
|
+
expiresIn: number;
|
|
754
|
+
sessionExpiresIn: number;
|
|
755
|
+
}>;
|
|
719
756
|
/** Redeem a decrypted unlock token for a session. */
|
|
720
757
|
exchangeUnlockToken(token: string): Promise<{
|
|
721
758
|
jwt: string;
|
package/dist/index.js
CHANGED
|
@@ -2193,6 +2193,17 @@ var ApiClient = class {
|
|
|
2193
2193
|
headers: { Authorization: `Bearer ${jwt}` }
|
|
2194
2194
|
});
|
|
2195
2195
|
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Trade a still-valid token for a fresh one, so an active user is never
|
|
2198
|
+
* signed out mid-use. The backend caps the whole chain by session age, so
|
|
2199
|
+
* this cannot renew forever; a 401 means "sign in again", not "retry".
|
|
2200
|
+
*/
|
|
2201
|
+
async refreshAccessToken(jwt) {
|
|
2202
|
+
return this._request("/auth/refresh", {
|
|
2203
|
+
method: "POST",
|
|
2204
|
+
headers: { Authorization: `Bearer ${jwt}` }
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2196
2207
|
/** Redeem a decrypted unlock token for a session. */
|
|
2197
2208
|
async exchangeUnlockToken(token) {
|
|
2198
2209
|
return this._request("/auth/unlock-token/exchange", {
|
|
@@ -2331,9 +2342,62 @@ import { sha256, encodeBase58 } from "ethers";
|
|
|
2331
2342
|
// src/social/teeClient.ts
|
|
2332
2343
|
import { startAuthentication } from "@simplewebauthn/browser";
|
|
2333
2344
|
|
|
2345
|
+
// src/social/attestation.ts
|
|
2346
|
+
import {
|
|
2347
|
+
getCollateral,
|
|
2348
|
+
getCollateralAndVerify,
|
|
2349
|
+
verify as verifyQuoteWithCollateral,
|
|
2350
|
+
PHALA_PCCS_URL
|
|
2351
|
+
} from "@phala/dcap-qvl";
|
|
2352
|
+
|
|
2353
|
+
// src/social/collateralCache.ts
|
|
2354
|
+
var CACHE_PREFIX = "decane:collateral:v1:";
|
|
2355
|
+
var COLLATERAL_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
2356
|
+
function storage() {
|
|
2357
|
+
try {
|
|
2358
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2359
|
+
} catch {
|
|
2360
|
+
return null;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
function keyFor(teeUrl) {
|
|
2364
|
+
return `${CACHE_PREFIX}${teeUrl}`;
|
|
2365
|
+
}
|
|
2366
|
+
function readCollateral(teeUrl) {
|
|
2367
|
+
const store = storage();
|
|
2368
|
+
if (!store) return null;
|
|
2369
|
+
try {
|
|
2370
|
+
const raw = store.getItem(keyFor(teeUrl));
|
|
2371
|
+
if (!raw) return null;
|
|
2372
|
+
const entry = JSON.parse(raw);
|
|
2373
|
+
if (!entry?.at || Date.now() - entry.at > COLLATERAL_TTL_MS) {
|
|
2374
|
+
store.removeItem(keyFor(teeUrl));
|
|
2375
|
+
return null;
|
|
2376
|
+
}
|
|
2377
|
+
return entry.collateral ?? null;
|
|
2378
|
+
} catch {
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
function writeCollateral(teeUrl, collateral) {
|
|
2383
|
+
const store = storage();
|
|
2384
|
+
if (!store) return;
|
|
2385
|
+
try {
|
|
2386
|
+
store.setItem(keyFor(teeUrl), JSON.stringify({ at: Date.now(), collateral }));
|
|
2387
|
+
} catch {
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
function dropCollateral(teeUrl) {
|
|
2391
|
+
const store = storage();
|
|
2392
|
+
if (!store) return;
|
|
2393
|
+
try {
|
|
2394
|
+
store.removeItem(keyFor(teeUrl));
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2334
2399
|
// src/social/attestation.ts
|
|
2335
2400
|
init_errors();
|
|
2336
|
-
import { getCollateralAndVerify } from "@phala/dcap-qvl";
|
|
2337
2401
|
|
|
2338
2402
|
// ../../infra/tee-measurement.json
|
|
2339
2403
|
var tee_measurement_default = {
|
|
@@ -2360,6 +2424,9 @@ function hexToBytes(hex) {
|
|
|
2360
2424
|
function bytesToHex(bytes) {
|
|
2361
2425
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2362
2426
|
}
|
|
2427
|
+
function nowSeconds() {
|
|
2428
|
+
return Math.floor(Date.now() / 1e3);
|
|
2429
|
+
}
|
|
2363
2430
|
async function verifyTeeAttestation(teeUrl, nonce) {
|
|
2364
2431
|
const res = await fetch(`${teeUrl}/attestation`, {
|
|
2365
2432
|
method: "POST",
|
|
@@ -2369,9 +2436,22 @@ async function verifyTeeAttestation(teeUrl, nonce) {
|
|
|
2369
2436
|
if (!res.ok) throw new AttestationError("quote fetch failed");
|
|
2370
2437
|
const { quote, measurement } = await res.json();
|
|
2371
2438
|
let verified;
|
|
2439
|
+
const cached = readCollateral(teeUrl);
|
|
2372
2440
|
try {
|
|
2373
|
-
|
|
2374
|
-
|
|
2441
|
+
if (cached) {
|
|
2442
|
+
try {
|
|
2443
|
+
verified = await verifyQuoteWithCollateral(hexToBytes(quote), cached, nowSeconds());
|
|
2444
|
+
} catch {
|
|
2445
|
+
dropCollateral(teeUrl);
|
|
2446
|
+
verified = await getCollateralAndVerify(hexToBytes(quote));
|
|
2447
|
+
}
|
|
2448
|
+
} else {
|
|
2449
|
+
const collateral = await getCollateral(PHALA_PCCS_URL, hexToBytes(quote));
|
|
2450
|
+
verified = await verifyQuoteWithCollateral(hexToBytes(quote), collateral, nowSeconds());
|
|
2451
|
+
writeCollateral(teeUrl, collateral);
|
|
2452
|
+
}
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
if (e instanceof AttestationError) throw e;
|
|
2375
2455
|
throw new AttestationError("quote signature invalid");
|
|
2376
2456
|
}
|
|
2377
2457
|
if (verified.status === "Revoked") {
|
|
@@ -2413,14 +2493,51 @@ async function teeFetch(url, init) {
|
|
|
2413
2493
|
}
|
|
2414
2494
|
var ATTESTATION_TTL_MS = 10 * 6e4;
|
|
2415
2495
|
var attestationCache = /* @__PURE__ */ new Map();
|
|
2496
|
+
var ATTESTATION_PERSIST_PREFIX = "decane:attested:v1:";
|
|
2497
|
+
function persistedAt(teeUrl) {
|
|
2498
|
+
try {
|
|
2499
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
2500
|
+
const raw = sessionStorage.getItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2501
|
+
const at = raw ? Number(raw) : NaN;
|
|
2502
|
+
return Number.isFinite(at) ? at : null;
|
|
2503
|
+
} catch {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function rememberAttested(teeUrl, at) {
|
|
2508
|
+
try {
|
|
2509
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2510
|
+
sessionStorage.setItem(ATTESTATION_PERSIST_PREFIX + teeUrl, String(at));
|
|
2511
|
+
}
|
|
2512
|
+
} catch {
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function forgetAttested(teeUrl) {
|
|
2516
|
+
try {
|
|
2517
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2518
|
+
sessionStorage.removeItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2519
|
+
}
|
|
2520
|
+
} catch {
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2416
2523
|
function attestOnce(teeUrl, verify) {
|
|
2417
2524
|
const hit = attestationCache.get(teeUrl);
|
|
2418
2525
|
if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
|
|
2419
|
-
const
|
|
2526
|
+
const at = persistedAt(teeUrl);
|
|
2527
|
+
if (at && Date.now() - at < ATTESTATION_TTL_MS) {
|
|
2528
|
+
const resolved = Promise.resolve();
|
|
2529
|
+
attestationCache.set(teeUrl, { at, promise: resolved });
|
|
2530
|
+
return resolved;
|
|
2531
|
+
}
|
|
2532
|
+
const startedAt = Date.now();
|
|
2533
|
+
const promise = verify().then(() => {
|
|
2534
|
+
rememberAttested(teeUrl, startedAt);
|
|
2535
|
+
}).catch((e) => {
|
|
2420
2536
|
attestationCache.delete(teeUrl);
|
|
2537
|
+
forgetAttested(teeUrl);
|
|
2421
2538
|
throw e;
|
|
2422
2539
|
});
|
|
2423
|
-
attestationCache.set(teeUrl, { at:
|
|
2540
|
+
attestationCache.set(teeUrl, { at: startedAt, promise });
|
|
2424
2541
|
return promise;
|
|
2425
2542
|
}
|
|
2426
2543
|
function prewarmAttestation(teeUrl) {
|
|
@@ -3692,29 +3809,44 @@ function clearLastUser(key) {
|
|
|
3692
3809
|
function sessionPersistKey(apiKey) {
|
|
3693
3810
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3694
3811
|
}
|
|
3812
|
+
var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3813
|
+
function useCrossTabSessionStore(enabled) {
|
|
3814
|
+
sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3815
|
+
}
|
|
3695
3816
|
function loadSession(key) {
|
|
3696
3817
|
try {
|
|
3697
|
-
const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
3698
|
-
|
|
3818
|
+
const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
|
|
3819
|
+
if (!raw) return null;
|
|
3820
|
+
const parsed = JSON.parse(raw);
|
|
3821
|
+
if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
|
|
3822
|
+
clearSessionPersist(key);
|
|
3823
|
+
return null;
|
|
3824
|
+
}
|
|
3825
|
+
return parsed;
|
|
3699
3826
|
} catch {
|
|
3700
3827
|
return null;
|
|
3701
3828
|
}
|
|
3702
3829
|
}
|
|
3703
3830
|
function saveSession(key, s) {
|
|
3704
3831
|
try {
|
|
3705
|
-
|
|
3832
|
+
sessionStore()?.setItem(key, JSON.stringify(s));
|
|
3706
3833
|
} catch {
|
|
3707
3834
|
}
|
|
3708
3835
|
}
|
|
3709
3836
|
function clearSessionPersist(key) {
|
|
3710
3837
|
try {
|
|
3711
|
-
sessionStorage.removeItem(key);
|
|
3838
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
|
|
3839
|
+
} catch {
|
|
3840
|
+
}
|
|
3841
|
+
try {
|
|
3842
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
3712
3843
|
} catch {
|
|
3713
3844
|
}
|
|
3714
3845
|
}
|
|
3715
3846
|
async function createDecaneConnect(config) {
|
|
3716
3847
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3717
3848
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3849
|
+
useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
|
|
3718
3850
|
if (!config.chains?.length)
|
|
3719
3851
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3720
3852
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3733,7 +3865,7 @@ async function createDecaneConnect(config) {
|
|
|
3733
3865
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3734
3866
|
const events = new EventEmitter();
|
|
3735
3867
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
3736
|
-
const
|
|
3868
|
+
const storage2 = new Storage();
|
|
3737
3869
|
const session = new SessionManager(
|
|
3738
3870
|
events,
|
|
3739
3871
|
durationMs,
|
|
@@ -3831,6 +3963,7 @@ async function createDecaneConnect(config) {
|
|
|
3831
3963
|
clearSessionPersist(SESSION_KEY);
|
|
3832
3964
|
}
|
|
3833
3965
|
let accessTokenExpiry = null;
|
|
3966
|
+
let tokenRefresh = null;
|
|
3834
3967
|
function scheduleAccessTokenExpiry(jwt, expiresAt) {
|
|
3835
3968
|
if (accessTokenExpiry) clearTimeout(accessTokenExpiry);
|
|
3836
3969
|
accessTokenExpiry = setTimeout(() => {
|
|
@@ -3840,6 +3973,40 @@ async function createDecaneConnect(config) {
|
|
|
3840
3973
|
dropLiveSession();
|
|
3841
3974
|
events.emit("session-expired");
|
|
3842
3975
|
}, Math.max(0, expiresAt - Date.now()));
|
|
3976
|
+
scheduleTokenRefresh(jwt, expiresAt);
|
|
3977
|
+
}
|
|
3978
|
+
function scheduleTokenRefresh(jwt, expiresAt) {
|
|
3979
|
+
if (tokenRefresh) clearTimeout(tokenRefresh);
|
|
3980
|
+
const lifetime = expiresAt - Date.now();
|
|
3981
|
+
if (lifetime <= 0) return;
|
|
3982
|
+
const at = Math.max(5e3, lifetime * 0.75);
|
|
3983
|
+
tokenRefresh = setTimeout(() => {
|
|
3984
|
+
tokenRefresh = null;
|
|
3985
|
+
void renewAccessToken(jwt);
|
|
3986
|
+
}, at);
|
|
3987
|
+
}
|
|
3988
|
+
async function renewAccessToken(expected) {
|
|
3989
|
+
if (auth?.jwt !== expected) return;
|
|
3990
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
3991
|
+
const onVisible = () => {
|
|
3992
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
3993
|
+
if (auth?.jwt === expected) void renewAccessToken(expected);
|
|
3994
|
+
};
|
|
3995
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
try {
|
|
3999
|
+
const { jwt: fresh } = await api.refreshAccessToken(expected);
|
|
4000
|
+
if (auth?.jwt !== expected) return;
|
|
4001
|
+
auth = { ...auth, jwt: fresh };
|
|
4002
|
+
const freshExpiry = decodeAccessToken(fresh)?.expiresAt;
|
|
4003
|
+
if (freshExpiry) {
|
|
4004
|
+
scheduleAccessTokenExpiry(fresh, freshExpiry);
|
|
4005
|
+
persistLiveSession();
|
|
4006
|
+
}
|
|
4007
|
+
events.emit("session-renewed");
|
|
4008
|
+
} catch {
|
|
4009
|
+
}
|
|
3843
4010
|
}
|
|
3844
4011
|
if (persistSession) {
|
|
3845
4012
|
const saved = loadSession(SESSION_KEY) ?? (LEGACY_SESSION_KEY !== SESSION_KEY ? loadSession(LEGACY_SESSION_KEY) : null);
|
|
@@ -3876,7 +4043,7 @@ async function createDecaneConnect(config) {
|
|
|
3876
4043
|
}
|
|
3877
4044
|
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3878
4045
|
try {
|
|
3879
|
-
const stored = await
|
|
4046
|
+
const stored = await storage2.getShare(userId);
|
|
3880
4047
|
if (!stored) return;
|
|
3881
4048
|
const { token } = await api.issueUnlockToken(jwt);
|
|
3882
4049
|
const unlockTokenIv = generateRandomBytes(12);
|
|
@@ -3885,13 +4052,13 @@ async function createDecaneConnect(config) {
|
|
|
3885
4052
|
new TextEncoder().encode(token),
|
|
3886
4053
|
unlockTokenIv
|
|
3887
4054
|
);
|
|
3888
|
-
await
|
|
4055
|
+
await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3889
4056
|
} catch (e) {
|
|
3890
4057
|
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3891
4058
|
}
|
|
3892
4059
|
}
|
|
3893
4060
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3894
|
-
const stored = await
|
|
4061
|
+
const stored = await storage2.getShare(userId);
|
|
3895
4062
|
if (!stored) {
|
|
3896
4063
|
throw new CallSequenceError(
|
|
3897
4064
|
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
@@ -3900,21 +4067,21 @@ async function createDecaneConnect(config) {
|
|
|
3900
4067
|
const wrappingKey = await getWrappingKey(stored);
|
|
3901
4068
|
const iv = generateRandomBytes(12);
|
|
3902
4069
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3903
|
-
await
|
|
4070
|
+
await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3904
4071
|
}
|
|
3905
4072
|
function hasPasswordUnlock(stored) {
|
|
3906
4073
|
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3907
4074
|
}
|
|
3908
4075
|
async function passwordDeviceCandidate() {
|
|
3909
4076
|
if (persisted?.userId) {
|
|
3910
|
-
const stored = await
|
|
4077
|
+
const stored = await storage2.getShare(persisted.userId);
|
|
3911
4078
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3912
4079
|
}
|
|
3913
4080
|
if (lastUserId) {
|
|
3914
|
-
const stored = await
|
|
4081
|
+
const stored = await storage2.getShare(lastUserId);
|
|
3915
4082
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3916
4083
|
}
|
|
3917
|
-
const candidates = (await
|
|
4084
|
+
const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
|
|
3918
4085
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3919
4086
|
}
|
|
3920
4087
|
async function getWrappingKey(stored) {
|
|
@@ -3942,7 +4109,7 @@ async function createDecaneConnect(config) {
|
|
|
3942
4109
|
let credentialId;
|
|
3943
4110
|
let prfOutput;
|
|
3944
4111
|
try {
|
|
3945
|
-
const label = (await
|
|
4112
|
+
const label = (await storage2.getShare(userId))?.profile?.email;
|
|
3946
4113
|
({ credentialId, prfOutput } = await registerWebAuthnCredential({
|
|
3947
4114
|
api,
|
|
3948
4115
|
jwt,
|
|
@@ -3974,8 +4141,8 @@ async function createDecaneConnect(config) {
|
|
|
3974
4141
|
const wrappingKey = await importAesKey(prfOutput);
|
|
3975
4142
|
const iv = generateRandomBytes(12);
|
|
3976
4143
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3977
|
-
const existing = await
|
|
3978
|
-
await
|
|
4144
|
+
const existing = await storage2.getShare(userId);
|
|
4145
|
+
await storage2.putShare({
|
|
3979
4146
|
...existing,
|
|
3980
4147
|
userId,
|
|
3981
4148
|
wrappedDeviceShare,
|
|
@@ -4059,7 +4226,7 @@ async function createDecaneConnect(config) {
|
|
|
4059
4226
|
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
4060
4227
|
const iv = generateRandomBytes(12);
|
|
4061
4228
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4062
|
-
await
|
|
4229
|
+
await storage2.putShare({
|
|
4063
4230
|
userId,
|
|
4064
4231
|
wrappedDeviceShare,
|
|
4065
4232
|
iv,
|
|
@@ -4133,7 +4300,7 @@ async function createDecaneConnect(config) {
|
|
|
4133
4300
|
shareSet.device,
|
|
4134
4301
|
iv
|
|
4135
4302
|
);
|
|
4136
|
-
await
|
|
4303
|
+
await storage2.putShare({
|
|
4137
4304
|
userId,
|
|
4138
4305
|
wrappedDeviceShare,
|
|
4139
4306
|
iv,
|
|
@@ -4237,7 +4404,7 @@ async function createDecaneConnect(config) {
|
|
|
4237
4404
|
async function returningUserUnlock(userId, jwt) {
|
|
4238
4405
|
const rawAddresses = await api.getAddresses(jwt);
|
|
4239
4406
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|
|
4240
|
-
const stored = await
|
|
4407
|
+
const stored = await storage2.getShare(userId);
|
|
4241
4408
|
let deviceShare;
|
|
4242
4409
|
let joinedViaPasskeySync = false;
|
|
4243
4410
|
if (!stored) {
|
|
@@ -4276,7 +4443,7 @@ async function createDecaneConnect(config) {
|
|
|
4276
4443
|
rpId,
|
|
4277
4444
|
teeUrl,
|
|
4278
4445
|
api,
|
|
4279
|
-
storage,
|
|
4446
|
+
storage: storage2,
|
|
4280
4447
|
onProgress: config.onRecoveryProgress,
|
|
4281
4448
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
4282
4449
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
@@ -4290,7 +4457,7 @@ async function createDecaneConnect(config) {
|
|
|
4290
4457
|
});
|
|
4291
4458
|
deviceShare = recovered.deviceShare;
|
|
4292
4459
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4293
|
-
if (!deferDeviceProtection && !await
|
|
4460
|
+
if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4294
4461
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4295
4462
|
}
|
|
4296
4463
|
} else {
|
|
@@ -4311,7 +4478,7 @@ async function createDecaneConnect(config) {
|
|
|
4311
4478
|
} catch (e) {
|
|
4312
4479
|
if (e instanceof StaleDeviceShareError) {
|
|
4313
4480
|
zeroize(deviceShare);
|
|
4314
|
-
await
|
|
4481
|
+
await storage2.deleteShare(userId);
|
|
4315
4482
|
if (stored) return returningUserUnlock(userId, jwt);
|
|
4316
4483
|
} else {
|
|
4317
4484
|
zeroize(deviceShare);
|
|
@@ -4335,7 +4502,7 @@ async function createDecaneConnect(config) {
|
|
|
4335
4502
|
const result = await tee.rotateShares(jwt);
|
|
4336
4503
|
try {
|
|
4337
4504
|
await persistRotatedDeviceShare(userId, result.deviceShare);
|
|
4338
|
-
const refreshed = await
|
|
4505
|
+
const refreshed = await storage2.getShare(userId);
|
|
4339
4506
|
if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
|
|
4340
4507
|
await api.putRecoveryBackup(jwt, {
|
|
4341
4508
|
wrappedDeviceShare: refreshed.wrappedDeviceShare,
|
|
@@ -4368,13 +4535,13 @@ async function createDecaneConnect(config) {
|
|
|
4368
4535
|
}
|
|
4369
4536
|
async function passkeyLoginCandidate() {
|
|
4370
4537
|
if (!persisted?.userId || !isPasskeySupported()) return null;
|
|
4371
|
-
return usablePasskeyShare(await
|
|
4538
|
+
return usablePasskeyShare(await storage2.getShare(persisted.userId));
|
|
4372
4539
|
}
|
|
4373
4540
|
async function passkeyDeviceCandidate() {
|
|
4374
4541
|
if (!isPasskeySupported()) return null;
|
|
4375
4542
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4376
|
-
if (lastUserId) return usablePasskeyShare(await
|
|
4377
|
-
const candidates = (await
|
|
4543
|
+
if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
|
|
4544
|
+
const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4378
4545
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4379
4546
|
}
|
|
4380
4547
|
async function canOfferPasskey() {
|
|
@@ -4382,15 +4549,15 @@ async function createDecaneConnect(config) {
|
|
|
4382
4549
|
if (await passkeyDeviceCandidate()) return true;
|
|
4383
4550
|
const known = persisted?.userId ?? lastUserId;
|
|
4384
4551
|
if (!known) return false;
|
|
4385
|
-
return await
|
|
4552
|
+
return await storage2.getShare(known) == null;
|
|
4386
4553
|
}
|
|
4387
4554
|
async function rememberProfile(userId, profile) {
|
|
4388
4555
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4389
4556
|
if (!Object.keys(incoming).length) return;
|
|
4390
4557
|
try {
|
|
4391
|
-
const stored = await
|
|
4558
|
+
const stored = await storage2.getShare(userId);
|
|
4392
4559
|
if (!stored) return;
|
|
4393
|
-
await
|
|
4560
|
+
await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4394
4561
|
} catch {
|
|
4395
4562
|
}
|
|
4396
4563
|
}
|
|
@@ -4414,7 +4581,7 @@ async function createDecaneConnect(config) {
|
|
|
4414
4581
|
savePersisted(PERSIST_KEY, persisted);
|
|
4415
4582
|
events.emit("connected");
|
|
4416
4583
|
persistLiveSession();
|
|
4417
|
-
const stored = await
|
|
4584
|
+
const stored = await storage2.getShare(userId);
|
|
4418
4585
|
return {
|
|
4419
4586
|
addresses,
|
|
4420
4587
|
isNewUser: false,
|
|
@@ -4587,7 +4754,7 @@ async function createDecaneConnect(config) {
|
|
|
4587
4754
|
events.emit("connected");
|
|
4588
4755
|
persistLiveSession();
|
|
4589
4756
|
scheduleAccessTokenExpiry(token, claims.expiresAt);
|
|
4590
|
-
const stored = await
|
|
4757
|
+
const stored = await storage2.getShare(claims.userId);
|
|
4591
4758
|
return {
|
|
4592
4759
|
addresses,
|
|
4593
4760
|
isNewUser: false,
|
|
@@ -4617,7 +4784,7 @@ async function createDecaneConnect(config) {
|
|
|
4617
4784
|
const candidate = await passkeyDeviceCandidate();
|
|
4618
4785
|
if (candidate) return passkeyLogin(candidate);
|
|
4619
4786
|
const known = persisted?.userId ?? lastUserId;
|
|
4620
|
-
if (isPasskeySupported() && known && await
|
|
4787
|
+
if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
|
|
4621
4788
|
return passkeyLoginWithoutShare(known);
|
|
4622
4789
|
}
|
|
4623
4790
|
throw new NewDeviceError();
|
|
@@ -4636,7 +4803,7 @@ async function createDecaneConnect(config) {
|
|
|
4636
4803
|
async setUnlockPassword(password) {
|
|
4637
4804
|
if (!auth) throw new ReconnectRequiredError();
|
|
4638
4805
|
validateUnlockPassword(password);
|
|
4639
|
-
const stored = await
|
|
4806
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4640
4807
|
if (!stored) throw new NewDeviceError();
|
|
4641
4808
|
const deviceShare = await aesGcmDecrypt(
|
|
4642
4809
|
await getWrappingKey(stored),
|
|
@@ -4655,7 +4822,7 @@ async function createDecaneConnect(config) {
|
|
|
4655
4822
|
);
|
|
4656
4823
|
const iv = generateRandomBytes(12);
|
|
4657
4824
|
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4658
|
-
await
|
|
4825
|
+
await storage2.putShare({
|
|
4659
4826
|
...stored,
|
|
4660
4827
|
unlockMethod: "password",
|
|
4661
4828
|
wrappedDeviceShare,
|
|
@@ -4735,9 +4902,9 @@ async function createDecaneConnect(config) {
|
|
|
4735
4902
|
async removeUnlockPassword() {
|
|
4736
4903
|
if (!auth) throw new ReconnectRequiredError();
|
|
4737
4904
|
await api.revokeUnlockTokens(auth.jwt);
|
|
4738
|
-
const stored = await
|
|
4905
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4739
4906
|
if (!stored || stored.unlockMethod !== "password") return;
|
|
4740
|
-
await
|
|
4907
|
+
await storage2.putShare({
|
|
4741
4908
|
...stored,
|
|
4742
4909
|
wrappedUnlockToken: void 0,
|
|
4743
4910
|
unlockTokenIv: void 0
|
|
@@ -4745,13 +4912,13 @@ async function createDecaneConnect(config) {
|
|
|
4745
4912
|
},
|
|
4746
4913
|
async hasPasskey() {
|
|
4747
4914
|
if (!auth) return false;
|
|
4748
|
-
const stored = await
|
|
4915
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4749
4916
|
return stored?.unlockMethod === "passkey";
|
|
4750
4917
|
},
|
|
4751
4918
|
async addPasskey() {
|
|
4752
4919
|
if (!auth) throw new ReconnectRequiredError();
|
|
4753
4920
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4754
|
-
const stored = await
|
|
4921
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4755
4922
|
if (stored?.unlockMethod === "passkey") return;
|
|
4756
4923
|
if (!stored) {
|
|
4757
4924
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4784,11 +4951,11 @@ async function createDecaneConnect(config) {
|
|
|
4784
4951
|
},
|
|
4785
4952
|
async deviceProtected() {
|
|
4786
4953
|
if (!auth) return false;
|
|
4787
|
-
return await
|
|
4954
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4788
4955
|
},
|
|
4789
4956
|
async protectDevice() {
|
|
4790
4957
|
if (!auth) throw new ReconnectRequiredError();
|
|
4791
|
-
if (await
|
|
4958
|
+
if (await storage2.getShare(auth.userId)) return true;
|
|
4792
4959
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4793
4960
|
const tee = new TeeClient(teeUrl);
|
|
4794
4961
|
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
@@ -4798,7 +4965,7 @@ async function createDecaneConnect(config) {
|
|
|
4798
4965
|
} finally {
|
|
4799
4966
|
zeroize(provisioned);
|
|
4800
4967
|
}
|
|
4801
|
-
return await
|
|
4968
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4802
4969
|
},
|
|
4803
4970
|
async unlock() {
|
|
4804
4971
|
if (!auth) {
|