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.cjs
CHANGED
|
@@ -2416,6 +2416,54 @@ var import_browser = require("@simplewebauthn/browser");
|
|
|
2416
2416
|
|
|
2417
2417
|
// src/social/attestation.ts
|
|
2418
2418
|
var import_dcap_qvl = require("@phala/dcap-qvl");
|
|
2419
|
+
|
|
2420
|
+
// src/social/collateralCache.ts
|
|
2421
|
+
var CACHE_PREFIX = "decane:collateral:v1:";
|
|
2422
|
+
var COLLATERAL_TTL_MS = 12 * 60 * 60 * 1e3;
|
|
2423
|
+
function storage() {
|
|
2424
|
+
try {
|
|
2425
|
+
return typeof localStorage !== "undefined" ? localStorage : null;
|
|
2426
|
+
} catch {
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function keyFor(teeUrl) {
|
|
2431
|
+
return `${CACHE_PREFIX}${teeUrl}`;
|
|
2432
|
+
}
|
|
2433
|
+
function readCollateral(teeUrl) {
|
|
2434
|
+
const store = storage();
|
|
2435
|
+
if (!store) return null;
|
|
2436
|
+
try {
|
|
2437
|
+
const raw = store.getItem(keyFor(teeUrl));
|
|
2438
|
+
if (!raw) return null;
|
|
2439
|
+
const entry = JSON.parse(raw);
|
|
2440
|
+
if (!entry?.at || Date.now() - entry.at > COLLATERAL_TTL_MS) {
|
|
2441
|
+
store.removeItem(keyFor(teeUrl));
|
|
2442
|
+
return null;
|
|
2443
|
+
}
|
|
2444
|
+
return entry.collateral ?? null;
|
|
2445
|
+
} catch {
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
function writeCollateral(teeUrl, collateral) {
|
|
2450
|
+
const store = storage();
|
|
2451
|
+
if (!store) return;
|
|
2452
|
+
try {
|
|
2453
|
+
store.setItem(keyFor(teeUrl), JSON.stringify({ at: Date.now(), collateral }));
|
|
2454
|
+
} catch {
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
function dropCollateral(teeUrl) {
|
|
2458
|
+
const store = storage();
|
|
2459
|
+
if (!store) return;
|
|
2460
|
+
try {
|
|
2461
|
+
store.removeItem(keyFor(teeUrl));
|
|
2462
|
+
} catch {
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
// src/social/attestation.ts
|
|
2419
2467
|
init_errors();
|
|
2420
2468
|
|
|
2421
2469
|
// ../../infra/tee-measurement.json
|
|
@@ -2443,6 +2491,9 @@ function hexToBytes(hex) {
|
|
|
2443
2491
|
function bytesToHex(bytes) {
|
|
2444
2492
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2445
2493
|
}
|
|
2494
|
+
function nowSeconds() {
|
|
2495
|
+
return Math.floor(Date.now() / 1e3);
|
|
2496
|
+
}
|
|
2446
2497
|
async function verifyTeeAttestation(teeUrl, nonce) {
|
|
2447
2498
|
const res = await fetch(`${teeUrl}/attestation`, {
|
|
2448
2499
|
method: "POST",
|
|
@@ -2452,9 +2503,22 @@ async function verifyTeeAttestation(teeUrl, nonce) {
|
|
|
2452
2503
|
if (!res.ok) throw new AttestationError("quote fetch failed");
|
|
2453
2504
|
const { quote, measurement } = await res.json();
|
|
2454
2505
|
let verified;
|
|
2506
|
+
const cached = readCollateral(teeUrl);
|
|
2455
2507
|
try {
|
|
2456
|
-
|
|
2457
|
-
|
|
2508
|
+
if (cached) {
|
|
2509
|
+
try {
|
|
2510
|
+
verified = await (0, import_dcap_qvl.verify)(hexToBytes(quote), cached, nowSeconds());
|
|
2511
|
+
} catch {
|
|
2512
|
+
dropCollateral(teeUrl);
|
|
2513
|
+
verified = await (0, import_dcap_qvl.getCollateralAndVerify)(hexToBytes(quote));
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
const collateral = await (0, import_dcap_qvl.getCollateral)(import_dcap_qvl.PHALA_PCCS_URL, hexToBytes(quote));
|
|
2517
|
+
verified = await (0, import_dcap_qvl.verify)(hexToBytes(quote), collateral, nowSeconds());
|
|
2518
|
+
writeCollateral(teeUrl, collateral);
|
|
2519
|
+
}
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
if (e instanceof AttestationError) throw e;
|
|
2458
2522
|
throw new AttestationError("quote signature invalid");
|
|
2459
2523
|
}
|
|
2460
2524
|
if (verified.status === "Revoked") {
|
|
@@ -2496,14 +2560,51 @@ async function teeFetch(url, init) {
|
|
|
2496
2560
|
}
|
|
2497
2561
|
var ATTESTATION_TTL_MS = 10 * 6e4;
|
|
2498
2562
|
var attestationCache = /* @__PURE__ */ new Map();
|
|
2563
|
+
var ATTESTATION_PERSIST_PREFIX = "decane:attested:v1:";
|
|
2564
|
+
function persistedAt(teeUrl) {
|
|
2565
|
+
try {
|
|
2566
|
+
if (typeof sessionStorage === "undefined") return null;
|
|
2567
|
+
const raw = sessionStorage.getItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2568
|
+
const at = raw ? Number(raw) : NaN;
|
|
2569
|
+
return Number.isFinite(at) ? at : null;
|
|
2570
|
+
} catch {
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function rememberAttested(teeUrl, at) {
|
|
2575
|
+
try {
|
|
2576
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2577
|
+
sessionStorage.setItem(ATTESTATION_PERSIST_PREFIX + teeUrl, String(at));
|
|
2578
|
+
}
|
|
2579
|
+
} catch {
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function forgetAttested(teeUrl) {
|
|
2583
|
+
try {
|
|
2584
|
+
if (typeof sessionStorage !== "undefined") {
|
|
2585
|
+
sessionStorage.removeItem(ATTESTATION_PERSIST_PREFIX + teeUrl);
|
|
2586
|
+
}
|
|
2587
|
+
} catch {
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2499
2590
|
function attestOnce(teeUrl, verify) {
|
|
2500
2591
|
const hit = attestationCache.get(teeUrl);
|
|
2501
2592
|
if (hit && Date.now() - hit.at < ATTESTATION_TTL_MS) return hit.promise;
|
|
2502
|
-
const
|
|
2593
|
+
const at = persistedAt(teeUrl);
|
|
2594
|
+
if (at && Date.now() - at < ATTESTATION_TTL_MS) {
|
|
2595
|
+
const resolved = Promise.resolve();
|
|
2596
|
+
attestationCache.set(teeUrl, { at, promise: resolved });
|
|
2597
|
+
return resolved;
|
|
2598
|
+
}
|
|
2599
|
+
const startedAt = Date.now();
|
|
2600
|
+
const promise = verify().then(() => {
|
|
2601
|
+
rememberAttested(teeUrl, startedAt);
|
|
2602
|
+
}).catch((e) => {
|
|
2503
2603
|
attestationCache.delete(teeUrl);
|
|
2604
|
+
forgetAttested(teeUrl);
|
|
2504
2605
|
throw e;
|
|
2505
2606
|
});
|
|
2506
|
-
attestationCache.set(teeUrl, { at:
|
|
2607
|
+
attestationCache.set(teeUrl, { at: startedAt, promise });
|
|
2507
2608
|
return promise;
|
|
2508
2609
|
}
|
|
2509
2610
|
function prewarmAttestation(teeUrl) {
|
|
@@ -3232,6 +3333,8 @@ function loginWithKingsChatPopup(opts) {
|
|
|
3232
3333
|
|
|
3233
3334
|
// src/social/signers/evm.ts
|
|
3234
3335
|
var import_ethers2 = require("ethers");
|
|
3336
|
+
init_errors();
|
|
3337
|
+
var DECODE = (bytes) => new TextDecoder().decode(bytes);
|
|
3235
3338
|
async function signEvmTransaction(tee, chain, tx, jwt) {
|
|
3236
3339
|
const resolved = await (0, import_ethers2.resolveProperties)(tx);
|
|
3237
3340
|
const unsigned = import_ethers2.Transaction.from(resolved).unsignedSerialized;
|
|
@@ -3240,7 +3343,44 @@ async function signEvmTransaction(tee, chain, tx, jwt) {
|
|
|
3240
3343
|
return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
|
|
3241
3344
|
}
|
|
3242
3345
|
async function signEvmMessage(tee, chain, message, jwt) {
|
|
3243
|
-
return tee.sign({ chain, kind: "message", payload: message }, jwt);
|
|
3346
|
+
return tee.sign({ chain, kind: "message", payload: messageAsString(message) }, jwt);
|
|
3347
|
+
}
|
|
3348
|
+
function messageAsString(message) {
|
|
3349
|
+
if (typeof message === "string") return message;
|
|
3350
|
+
if (!isValidUtf8(message)) {
|
|
3351
|
+
throw new CallSequenceError(
|
|
3352
|
+
"signMessage: the message bytes are not valid UTF-8. The signer hashes text; pass a string, or UTF-8 bytes."
|
|
3353
|
+
);
|
|
3354
|
+
}
|
|
3355
|
+
return DECODE(message);
|
|
3356
|
+
}
|
|
3357
|
+
function isValidUtf8(b) {
|
|
3358
|
+
let i = 0;
|
|
3359
|
+
while (i < b.length) {
|
|
3360
|
+
const c = b[i];
|
|
3361
|
+
let n = 0;
|
|
3362
|
+
if (c < 128) n = 0;
|
|
3363
|
+
else if (c >= 194 && c <= 223) n = 1;
|
|
3364
|
+
else if (c >= 224 && c <= 239) n = 2;
|
|
3365
|
+
else if (c >= 240 && c <= 244) n = 3;
|
|
3366
|
+
else return false;
|
|
3367
|
+
for (let k = 1; k <= n; k++) {
|
|
3368
|
+
const t = b[i + k];
|
|
3369
|
+
if (t === void 0 || (t & 192) !== 128) return false;
|
|
3370
|
+
}
|
|
3371
|
+
if (n === 2) {
|
|
3372
|
+
const c1 = b[i + 1];
|
|
3373
|
+
if (c === 224 && c1 < 160) return false;
|
|
3374
|
+
if (c === 237 && c1 >= 160) return false;
|
|
3375
|
+
}
|
|
3376
|
+
if (n === 3) {
|
|
3377
|
+
const c1 = b[i + 1];
|
|
3378
|
+
if (c === 240 && c1 < 144) return false;
|
|
3379
|
+
if (c === 244 && c1 >= 144) return false;
|
|
3380
|
+
}
|
|
3381
|
+
i += n + 1;
|
|
3382
|
+
}
|
|
3383
|
+
return true;
|
|
3244
3384
|
}
|
|
3245
3385
|
async function signEvmAuthorization(tee, chainId, address, nonce, jwt) {
|
|
3246
3386
|
const serialized = await tee.sign(
|
|
@@ -3736,29 +3876,44 @@ function clearLastUser(key) {
|
|
|
3736
3876
|
function sessionPersistKey(apiKey) {
|
|
3737
3877
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3738
3878
|
}
|
|
3879
|
+
var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3880
|
+
function useCrossTabSessionStore(enabled) {
|
|
3881
|
+
sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
|
|
3882
|
+
}
|
|
3739
3883
|
function loadSession(key) {
|
|
3740
3884
|
try {
|
|
3741
|
-
const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
|
|
3742
|
-
|
|
3885
|
+
const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
|
|
3886
|
+
if (!raw) return null;
|
|
3887
|
+
const parsed = JSON.parse(raw);
|
|
3888
|
+
if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
|
|
3889
|
+
clearSessionPersist(key);
|
|
3890
|
+
return null;
|
|
3891
|
+
}
|
|
3892
|
+
return parsed;
|
|
3743
3893
|
} catch {
|
|
3744
3894
|
return null;
|
|
3745
3895
|
}
|
|
3746
3896
|
}
|
|
3747
3897
|
function saveSession(key, s) {
|
|
3748
3898
|
try {
|
|
3749
|
-
|
|
3899
|
+
sessionStore()?.setItem(key, JSON.stringify(s));
|
|
3750
3900
|
} catch {
|
|
3751
3901
|
}
|
|
3752
3902
|
}
|
|
3753
3903
|
function clearSessionPersist(key) {
|
|
3754
3904
|
try {
|
|
3755
|
-
sessionStorage.removeItem(key);
|
|
3905
|
+
if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
|
|
3906
|
+
} catch {
|
|
3907
|
+
}
|
|
3908
|
+
try {
|
|
3909
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(key);
|
|
3756
3910
|
} catch {
|
|
3757
3911
|
}
|
|
3758
3912
|
}
|
|
3759
3913
|
async function createDecaneConnect(config) {
|
|
3760
3914
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3761
3915
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3916
|
+
useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
|
|
3762
3917
|
if (!config.chains?.length)
|
|
3763
3918
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3764
3919
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3777,7 +3932,7 @@ async function createDecaneConnect(config) {
|
|
|
3777
3932
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3778
3933
|
const events = new EventEmitter();
|
|
3779
3934
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
3780
|
-
const
|
|
3935
|
+
const storage2 = new Storage();
|
|
3781
3936
|
const session = new SessionManager(
|
|
3782
3937
|
events,
|
|
3783
3938
|
durationMs,
|
|
@@ -3920,7 +4075,7 @@ async function createDecaneConnect(config) {
|
|
|
3920
4075
|
}
|
|
3921
4076
|
async function attachUnlockToken(userId, jwt, tokenKey) {
|
|
3922
4077
|
try {
|
|
3923
|
-
const stored = await
|
|
4078
|
+
const stored = await storage2.getShare(userId);
|
|
3924
4079
|
if (!stored) return;
|
|
3925
4080
|
const { token } = await api.issueUnlockToken(jwt);
|
|
3926
4081
|
const unlockTokenIv = generateRandomBytes(12);
|
|
@@ -3929,13 +4084,13 @@ async function createDecaneConnect(config) {
|
|
|
3929
4084
|
new TextEncoder().encode(token),
|
|
3930
4085
|
unlockTokenIv
|
|
3931
4086
|
);
|
|
3932
|
-
await
|
|
4087
|
+
await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
|
|
3933
4088
|
} catch (e) {
|
|
3934
4089
|
console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
|
|
3935
4090
|
}
|
|
3936
4091
|
}
|
|
3937
4092
|
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
3938
|
-
const stored = await
|
|
4093
|
+
const stored = await storage2.getShare(userId);
|
|
3939
4094
|
if (!stored) {
|
|
3940
4095
|
throw new CallSequenceError(
|
|
3941
4096
|
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
@@ -3944,21 +4099,21 @@ async function createDecaneConnect(config) {
|
|
|
3944
4099
|
const wrappingKey = await getWrappingKey(stored);
|
|
3945
4100
|
const iv = generateRandomBytes(12);
|
|
3946
4101
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
3947
|
-
await
|
|
4102
|
+
await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
3948
4103
|
}
|
|
3949
4104
|
function hasPasswordUnlock(stored) {
|
|
3950
4105
|
return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
|
|
3951
4106
|
}
|
|
3952
4107
|
async function passwordDeviceCandidate() {
|
|
3953
4108
|
if (persisted?.userId) {
|
|
3954
|
-
const stored = await
|
|
4109
|
+
const stored = await storage2.getShare(persisted.userId);
|
|
3955
4110
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3956
4111
|
}
|
|
3957
4112
|
if (lastUserId) {
|
|
3958
|
-
const stored = await
|
|
4113
|
+
const stored = await storage2.getShare(lastUserId);
|
|
3959
4114
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3960
4115
|
}
|
|
3961
|
-
const candidates = (await
|
|
4116
|
+
const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
|
|
3962
4117
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3963
4118
|
}
|
|
3964
4119
|
async function getWrappingKey(stored) {
|
|
@@ -3986,7 +4141,7 @@ async function createDecaneConnect(config) {
|
|
|
3986
4141
|
let credentialId;
|
|
3987
4142
|
let prfOutput;
|
|
3988
4143
|
try {
|
|
3989
|
-
const label = (await
|
|
4144
|
+
const label = (await storage2.getShare(userId))?.profile?.email;
|
|
3990
4145
|
({ credentialId, prfOutput } = await registerWebAuthnCredential({
|
|
3991
4146
|
api,
|
|
3992
4147
|
jwt,
|
|
@@ -4018,8 +4173,8 @@ async function createDecaneConnect(config) {
|
|
|
4018
4173
|
const wrappingKey = await importAesKey(prfOutput);
|
|
4019
4174
|
const iv = generateRandomBytes(12);
|
|
4020
4175
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4021
|
-
const existing = await
|
|
4022
|
-
await
|
|
4176
|
+
const existing = await storage2.getShare(userId);
|
|
4177
|
+
await storage2.putShare({
|
|
4023
4178
|
...existing,
|
|
4024
4179
|
userId,
|
|
4025
4180
|
wrappedDeviceShare,
|
|
@@ -4103,7 +4258,7 @@ async function createDecaneConnect(config) {
|
|
|
4103
4258
|
const { wrappingKey, tokenKey, meta } = await passwordSetup();
|
|
4104
4259
|
const iv = generateRandomBytes(12);
|
|
4105
4260
|
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
4106
|
-
await
|
|
4261
|
+
await storage2.putShare({
|
|
4107
4262
|
userId,
|
|
4108
4263
|
wrappedDeviceShare,
|
|
4109
4264
|
iv,
|
|
@@ -4177,7 +4332,7 @@ async function createDecaneConnect(config) {
|
|
|
4177
4332
|
shareSet.device,
|
|
4178
4333
|
iv
|
|
4179
4334
|
);
|
|
4180
|
-
await
|
|
4335
|
+
await storage2.putShare({
|
|
4181
4336
|
userId,
|
|
4182
4337
|
wrappedDeviceShare,
|
|
4183
4338
|
iv,
|
|
@@ -4281,7 +4436,7 @@ async function createDecaneConnect(config) {
|
|
|
4281
4436
|
async function returningUserUnlock(userId, jwt) {
|
|
4282
4437
|
const rawAddresses = await api.getAddresses(jwt);
|
|
4283
4438
|
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
|
|
4284
|
-
const stored = await
|
|
4439
|
+
const stored = await storage2.getShare(userId);
|
|
4285
4440
|
let deviceShare;
|
|
4286
4441
|
let joinedViaPasskeySync = false;
|
|
4287
4442
|
if (!stored) {
|
|
@@ -4320,7 +4475,7 @@ async function createDecaneConnect(config) {
|
|
|
4320
4475
|
rpId,
|
|
4321
4476
|
teeUrl,
|
|
4322
4477
|
api,
|
|
4323
|
-
storage,
|
|
4478
|
+
storage: storage2,
|
|
4324
4479
|
onProgress: config.onRecoveryProgress,
|
|
4325
4480
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
4326
4481
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
@@ -4334,7 +4489,7 @@ async function createDecaneConnect(config) {
|
|
|
4334
4489
|
});
|
|
4335
4490
|
deviceShare = recovered.deviceShare;
|
|
4336
4491
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4337
|
-
if (!deferDeviceProtection && !await
|
|
4492
|
+
if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4338
4493
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4339
4494
|
}
|
|
4340
4495
|
} else {
|
|
@@ -4355,7 +4510,7 @@ async function createDecaneConnect(config) {
|
|
|
4355
4510
|
} catch (e) {
|
|
4356
4511
|
if (e instanceof StaleDeviceShareError) {
|
|
4357
4512
|
zeroize(deviceShare);
|
|
4358
|
-
await
|
|
4513
|
+
await storage2.deleteShare(userId);
|
|
4359
4514
|
if (stored) return returningUserUnlock(userId, jwt);
|
|
4360
4515
|
} else {
|
|
4361
4516
|
zeroize(deviceShare);
|
|
@@ -4379,7 +4534,7 @@ async function createDecaneConnect(config) {
|
|
|
4379
4534
|
const result = await tee.rotateShares(jwt);
|
|
4380
4535
|
try {
|
|
4381
4536
|
await persistRotatedDeviceShare(userId, result.deviceShare);
|
|
4382
|
-
const refreshed = await
|
|
4537
|
+
const refreshed = await storage2.getShare(userId);
|
|
4383
4538
|
if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
|
|
4384
4539
|
await api.putRecoveryBackup(jwt, {
|
|
4385
4540
|
wrappedDeviceShare: refreshed.wrappedDeviceShare,
|
|
@@ -4412,13 +4567,13 @@ async function createDecaneConnect(config) {
|
|
|
4412
4567
|
}
|
|
4413
4568
|
async function passkeyLoginCandidate() {
|
|
4414
4569
|
if (!persisted?.userId || !isPasskeySupported()) return null;
|
|
4415
|
-
return usablePasskeyShare(await
|
|
4570
|
+
return usablePasskeyShare(await storage2.getShare(persisted.userId));
|
|
4416
4571
|
}
|
|
4417
4572
|
async function passkeyDeviceCandidate() {
|
|
4418
4573
|
if (!isPasskeySupported()) return null;
|
|
4419
4574
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4420
|
-
if (lastUserId) return usablePasskeyShare(await
|
|
4421
|
-
const candidates = (await
|
|
4575
|
+
if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
|
|
4576
|
+
const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4422
4577
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4423
4578
|
}
|
|
4424
4579
|
async function canOfferPasskey() {
|
|
@@ -4426,15 +4581,15 @@ async function createDecaneConnect(config) {
|
|
|
4426
4581
|
if (await passkeyDeviceCandidate()) return true;
|
|
4427
4582
|
const known = persisted?.userId ?? lastUserId;
|
|
4428
4583
|
if (!known) return false;
|
|
4429
|
-
return await
|
|
4584
|
+
return await storage2.getShare(known) == null;
|
|
4430
4585
|
}
|
|
4431
4586
|
async function rememberProfile(userId, profile) {
|
|
4432
4587
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
4433
4588
|
if (!Object.keys(incoming).length) return;
|
|
4434
4589
|
try {
|
|
4435
|
-
const stored = await
|
|
4590
|
+
const stored = await storage2.getShare(userId);
|
|
4436
4591
|
if (!stored) return;
|
|
4437
|
-
await
|
|
4592
|
+
await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
|
|
4438
4593
|
} catch {
|
|
4439
4594
|
}
|
|
4440
4595
|
}
|
|
@@ -4458,7 +4613,7 @@ async function createDecaneConnect(config) {
|
|
|
4458
4613
|
savePersisted(PERSIST_KEY, persisted);
|
|
4459
4614
|
events.emit("connected");
|
|
4460
4615
|
persistLiveSession();
|
|
4461
|
-
const stored = await
|
|
4616
|
+
const stored = await storage2.getShare(userId);
|
|
4462
4617
|
return {
|
|
4463
4618
|
addresses,
|
|
4464
4619
|
isNewUser: false,
|
|
@@ -4631,7 +4786,7 @@ async function createDecaneConnect(config) {
|
|
|
4631
4786
|
events.emit("connected");
|
|
4632
4787
|
persistLiveSession();
|
|
4633
4788
|
scheduleAccessTokenExpiry(token, claims.expiresAt);
|
|
4634
|
-
const stored = await
|
|
4789
|
+
const stored = await storage2.getShare(claims.userId);
|
|
4635
4790
|
return {
|
|
4636
4791
|
addresses,
|
|
4637
4792
|
isNewUser: false,
|
|
@@ -4661,7 +4816,7 @@ async function createDecaneConnect(config) {
|
|
|
4661
4816
|
const candidate = await passkeyDeviceCandidate();
|
|
4662
4817
|
if (candidate) return passkeyLogin(candidate);
|
|
4663
4818
|
const known = persisted?.userId ?? lastUserId;
|
|
4664
|
-
if (isPasskeySupported() && known && await
|
|
4819
|
+
if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
|
|
4665
4820
|
return passkeyLoginWithoutShare(known);
|
|
4666
4821
|
}
|
|
4667
4822
|
throw new NewDeviceError();
|
|
@@ -4680,7 +4835,7 @@ async function createDecaneConnect(config) {
|
|
|
4680
4835
|
async setUnlockPassword(password) {
|
|
4681
4836
|
if (!auth) throw new ReconnectRequiredError();
|
|
4682
4837
|
validateUnlockPassword(password);
|
|
4683
|
-
const stored = await
|
|
4838
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4684
4839
|
if (!stored) throw new NewDeviceError();
|
|
4685
4840
|
const deviceShare = await aesGcmDecrypt(
|
|
4686
4841
|
await getWrappingKey(stored),
|
|
@@ -4699,7 +4854,7 @@ async function createDecaneConnect(config) {
|
|
|
4699
4854
|
);
|
|
4700
4855
|
const iv = generateRandomBytes(12);
|
|
4701
4856
|
const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
|
|
4702
|
-
await
|
|
4857
|
+
await storage2.putShare({
|
|
4703
4858
|
...stored,
|
|
4704
4859
|
unlockMethod: "password",
|
|
4705
4860
|
wrappedDeviceShare,
|
|
@@ -4779,9 +4934,9 @@ async function createDecaneConnect(config) {
|
|
|
4779
4934
|
async removeUnlockPassword() {
|
|
4780
4935
|
if (!auth) throw new ReconnectRequiredError();
|
|
4781
4936
|
await api.revokeUnlockTokens(auth.jwt);
|
|
4782
|
-
const stored = await
|
|
4937
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4783
4938
|
if (!stored || stored.unlockMethod !== "password") return;
|
|
4784
|
-
await
|
|
4939
|
+
await storage2.putShare({
|
|
4785
4940
|
...stored,
|
|
4786
4941
|
wrappedUnlockToken: void 0,
|
|
4787
4942
|
unlockTokenIv: void 0
|
|
@@ -4789,13 +4944,13 @@ async function createDecaneConnect(config) {
|
|
|
4789
4944
|
},
|
|
4790
4945
|
async hasPasskey() {
|
|
4791
4946
|
if (!auth) return false;
|
|
4792
|
-
const stored = await
|
|
4947
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4793
4948
|
return stored?.unlockMethod === "passkey";
|
|
4794
4949
|
},
|
|
4795
4950
|
async addPasskey() {
|
|
4796
4951
|
if (!auth) throw new ReconnectRequiredError();
|
|
4797
4952
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4798
|
-
const stored = await
|
|
4953
|
+
const stored = await storage2.getShare(auth.userId);
|
|
4799
4954
|
if (stored?.unlockMethod === "passkey") return;
|
|
4800
4955
|
if (!stored) {
|
|
4801
4956
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4828,11 +4983,11 @@ async function createDecaneConnect(config) {
|
|
|
4828
4983
|
},
|
|
4829
4984
|
async deviceProtected() {
|
|
4830
4985
|
if (!auth) return false;
|
|
4831
|
-
return await
|
|
4986
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4832
4987
|
},
|
|
4833
4988
|
async protectDevice() {
|
|
4834
4989
|
if (!auth) throw new ReconnectRequiredError();
|
|
4835
|
-
if (await
|
|
4990
|
+
if (await storage2.getShare(auth.userId)) return true;
|
|
4836
4991
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4837
4992
|
const tee = new TeeClient(teeUrl);
|
|
4838
4993
|
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
@@ -4842,7 +4997,7 @@ async function createDecaneConnect(config) {
|
|
|
4842
4997
|
} finally {
|
|
4843
4998
|
zeroize(provisioned);
|
|
4844
4999
|
}
|
|
4845
|
-
return await
|
|
5000
|
+
return await storage2.getShare(auth.userId) !== null;
|
|
4846
5001
|
},
|
|
4847
5002
|
async unlock() {
|
|
4848
5003
|
if (!auth) {
|
|
@@ -4884,8 +5039,7 @@ async function createDecaneConnect(config) {
|
|
|
4884
5039
|
return bufferToB64u(sig);
|
|
4885
5040
|
}
|
|
4886
5041
|
if (req.chain.startsWith("tron:")) {
|
|
4887
|
-
|
|
4888
|
-
return tee.sign({ chain: req.chain, kind: "message", payload: msg }, auth?.jwt);
|
|
5042
|
+
return tee.sign({ chain: req.chain, kind: "message", payload: messageAsString(req.message) }, auth?.jwt);
|
|
4889
5043
|
}
|
|
4890
5044
|
throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
|
|
4891
5045
|
},
|