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.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
- verified = await getCollateralAndVerify(hexToBytes(quote));
2374
- } catch {
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 promise = verify().catch((e) => {
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: Date.now(), promise });
2529
+ attestationCache.set(teeUrl, { at: startedAt, promise });
2424
2530
  return promise;
2425
2531
  }
2426
2532
  function prewarmAttestation(teeUrl) {
@@ -3148,7 +3254,9 @@ function loginWithKingsChatPopup(opts) {
3148
3254
  }
3149
3255
 
3150
3256
  // src/social/signers/evm.ts
3257
+ init_errors();
3151
3258
  import { keccak256, Transaction, resolveProperties } from "ethers";
3259
+ var DECODE = (bytes) => new TextDecoder().decode(bytes);
3152
3260
  async function signEvmTransaction(tee, chain, tx, jwt) {
3153
3261
  const resolved = await resolveProperties(tx);
3154
3262
  const unsigned = Transaction.from(resolved).unsignedSerialized;
@@ -3157,7 +3265,44 @@ async function signEvmTransaction(tee, chain, tx, jwt) {
3157
3265
  return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
3158
3266
  }
3159
3267
  async function signEvmMessage(tee, chain, message, jwt) {
3160
- return tee.sign({ chain, kind: "message", payload: message }, jwt);
3268
+ return tee.sign({ chain, kind: "message", payload: messageAsString(message) }, jwt);
3269
+ }
3270
+ function messageAsString(message) {
3271
+ if (typeof message === "string") return message;
3272
+ if (!isValidUtf8(message)) {
3273
+ throw new CallSequenceError(
3274
+ "signMessage: the message bytes are not valid UTF-8. The signer hashes text; pass a string, or UTF-8 bytes."
3275
+ );
3276
+ }
3277
+ return DECODE(message);
3278
+ }
3279
+ function isValidUtf8(b) {
3280
+ let i = 0;
3281
+ while (i < b.length) {
3282
+ const c = b[i];
3283
+ let n = 0;
3284
+ if (c < 128) n = 0;
3285
+ else if (c >= 194 && c <= 223) n = 1;
3286
+ else if (c >= 224 && c <= 239) n = 2;
3287
+ else if (c >= 240 && c <= 244) n = 3;
3288
+ else return false;
3289
+ for (let k = 1; k <= n; k++) {
3290
+ const t = b[i + k];
3291
+ if (t === void 0 || (t & 192) !== 128) return false;
3292
+ }
3293
+ if (n === 2) {
3294
+ const c1 = b[i + 1];
3295
+ if (c === 224 && c1 < 160) return false;
3296
+ if (c === 237 && c1 >= 160) return false;
3297
+ }
3298
+ if (n === 3) {
3299
+ const c1 = b[i + 1];
3300
+ if (c === 240 && c1 < 144) return false;
3301
+ if (c === 244 && c1 >= 144) return false;
3302
+ }
3303
+ i += n + 1;
3304
+ }
3305
+ return true;
3161
3306
  }
3162
3307
  async function signEvmAuthorization(tee, chainId, address, nonce, jwt) {
3163
3308
  const serialized = await tee.sign(
@@ -3653,29 +3798,44 @@ function clearLastUser(key) {
3653
3798
  function sessionPersistKey(apiKey) {
3654
3799
  return `decane:sess:${apiKey.slice(0, 20)}`;
3655
3800
  }
3801
+ var sessionStore = () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
3802
+ function useCrossTabSessionStore(enabled) {
3803
+ sessionStore = enabled ? () => typeof localStorage !== "undefined" ? localStorage : null : () => typeof sessionStorage !== "undefined" ? sessionStorage : null;
3804
+ }
3656
3805
  function loadSession(key) {
3657
3806
  try {
3658
- const raw = typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null;
3659
- return raw ? JSON.parse(raw) : null;
3807
+ const raw = sessionStore()?.getItem(key) ?? (typeof sessionStorage !== "undefined" ? sessionStorage.getItem(key) : null) ?? (typeof localStorage !== "undefined" ? localStorage.getItem(key) : null);
3808
+ if (!raw) return null;
3809
+ const parsed = JSON.parse(raw);
3810
+ if (parsed?.expiresAt && parsed.expiresAt <= Date.now()) {
3811
+ clearSessionPersist(key);
3812
+ return null;
3813
+ }
3814
+ return parsed;
3660
3815
  } catch {
3661
3816
  return null;
3662
3817
  }
3663
3818
  }
3664
3819
  function saveSession(key, s) {
3665
3820
  try {
3666
- sessionStorage.setItem(key, JSON.stringify(s));
3821
+ sessionStore()?.setItem(key, JSON.stringify(s));
3667
3822
  } catch {
3668
3823
  }
3669
3824
  }
3670
3825
  function clearSessionPersist(key) {
3671
3826
  try {
3672
- sessionStorage.removeItem(key);
3827
+ if (typeof sessionStorage !== "undefined") sessionStorage.removeItem(key);
3828
+ } catch {
3829
+ }
3830
+ try {
3831
+ if (typeof localStorage !== "undefined") localStorage.removeItem(key);
3673
3832
  } catch {
3674
3833
  }
3675
3834
  }
3676
3835
  async function createDecaneConnect(config) {
3677
3836
  if (!config.appId) throw new Error("decane-connect-kit: appId is required");
3678
3837
  if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
3838
+ useCrossTabSessionStore(config.resumeSessionAcrossTabs === true);
3679
3839
  if (!config.chains?.length)
3680
3840
  throw new Error("decane-connect-kit: at least one chain is required");
3681
3841
  if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
@@ -3694,7 +3854,7 @@ async function createDecaneConnect(config) {
3694
3854
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3695
3855
  const events = new EventEmitter();
3696
3856
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
3697
- const storage = new Storage();
3857
+ const storage2 = new Storage();
3698
3858
  const session = new SessionManager(
3699
3859
  events,
3700
3860
  durationMs,
@@ -3837,7 +3997,7 @@ async function createDecaneConnect(config) {
3837
3997
  }
3838
3998
  async function attachUnlockToken(userId, jwt, tokenKey) {
3839
3999
  try {
3840
- const stored = await storage.getShare(userId);
4000
+ const stored = await storage2.getShare(userId);
3841
4001
  if (!stored) return;
3842
4002
  const { token } = await api.issueUnlockToken(jwt);
3843
4003
  const unlockTokenIv = generateRandomBytes(12);
@@ -3846,13 +4006,13 @@ async function createDecaneConnect(config) {
3846
4006
  new TextEncoder().encode(token),
3847
4007
  unlockTokenIv
3848
4008
  );
3849
- await storage.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
4009
+ await storage2.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
3850
4010
  } catch (e) {
3851
4011
  console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
3852
4012
  }
3853
4013
  }
3854
4014
  async function persistRotatedDeviceShare(userId, deviceShare) {
3855
- const stored = await storage.getShare(userId);
4015
+ const stored = await storage2.getShare(userId);
3856
4016
  if (!stored) {
3857
4017
  throw new CallSequenceError(
3858
4018
  "Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
@@ -3861,21 +4021,21 @@ async function createDecaneConnect(config) {
3861
4021
  const wrappingKey = await getWrappingKey(stored);
3862
4022
  const iv = generateRandomBytes(12);
3863
4023
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3864
- await storage.putShare({ ...stored, wrappedDeviceShare, iv });
4024
+ await storage2.putShare({ ...stored, wrappedDeviceShare, iv });
3865
4025
  }
3866
4026
  function hasPasswordUnlock(stored) {
3867
4027
  return !!stored && stored.unlockMethod === "password" && !!stored.passwordSalt && !!stored.wrappedUnlockToken && !!stored.unlockTokenIv;
3868
4028
  }
3869
4029
  async function passwordDeviceCandidate() {
3870
4030
  if (persisted?.userId) {
3871
- const stored = await storage.getShare(persisted.userId);
4031
+ const stored = await storage2.getShare(persisted.userId);
3872
4032
  return hasPasswordUnlock(stored) ? stored : null;
3873
4033
  }
3874
4034
  if (lastUserId) {
3875
- const stored = await storage.getShare(lastUserId);
4035
+ const stored = await storage2.getShare(lastUserId);
3876
4036
  return hasPasswordUnlock(stored) ? stored : null;
3877
4037
  }
3878
- const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
4038
+ const candidates = (await storage2.listShares()).filter(hasPasswordUnlock);
3879
4039
  return candidates.length === 1 ? candidates[0] : null;
3880
4040
  }
3881
4041
  async function getWrappingKey(stored) {
@@ -3903,7 +4063,7 @@ async function createDecaneConnect(config) {
3903
4063
  let credentialId;
3904
4064
  let prfOutput;
3905
4065
  try {
3906
- const label = (await storage.getShare(userId))?.profile?.email;
4066
+ const label = (await storage2.getShare(userId))?.profile?.email;
3907
4067
  ({ credentialId, prfOutput } = await registerWebAuthnCredential({
3908
4068
  api,
3909
4069
  jwt,
@@ -3935,8 +4095,8 @@ async function createDecaneConnect(config) {
3935
4095
  const wrappingKey = await importAesKey(prfOutput);
3936
4096
  const iv = generateRandomBytes(12);
3937
4097
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3938
- const existing = await storage.getShare(userId);
3939
- await storage.putShare({
4098
+ const existing = await storage2.getShare(userId);
4099
+ await storage2.putShare({
3940
4100
  ...existing,
3941
4101
  userId,
3942
4102
  wrappedDeviceShare,
@@ -4020,7 +4180,7 @@ async function createDecaneConnect(config) {
4020
4180
  const { wrappingKey, tokenKey, meta } = await passwordSetup();
4021
4181
  const iv = generateRandomBytes(12);
4022
4182
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
4023
- await storage.putShare({
4183
+ await storage2.putShare({
4024
4184
  userId,
4025
4185
  wrappedDeviceShare,
4026
4186
  iv,
@@ -4094,7 +4254,7 @@ async function createDecaneConnect(config) {
4094
4254
  shareSet.device,
4095
4255
  iv
4096
4256
  );
4097
- await storage.putShare({
4257
+ await storage2.putShare({
4098
4258
  userId,
4099
4259
  wrappedDeviceShare,
4100
4260
  iv,
@@ -4198,7 +4358,7 @@ async function createDecaneConnect(config) {
4198
4358
  async function returningUserUnlock(userId, jwt) {
4199
4359
  const rawAddresses = await api.getAddresses(jwt);
4200
4360
  const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana, tron: rawAddresses.tron ?? evmToTronAddress2(rawAddresses.evm) } : null;
4201
- const stored = await storage.getShare(userId);
4361
+ const stored = await storage2.getShare(userId);
4202
4362
  let deviceShare;
4203
4363
  let joinedViaPasskeySync = false;
4204
4364
  if (!stored) {
@@ -4237,7 +4397,7 @@ async function createDecaneConnect(config) {
4237
4397
  rpId,
4238
4398
  teeUrl,
4239
4399
  api,
4240
- storage,
4400
+ storage: storage2,
4241
4401
  onProgress: config.onRecoveryProgress,
4242
4402
  promptForRecoveryFile: config.promptForRecoveryFile,
4243
4403
  onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
@@ -4251,7 +4411,7 @@ async function createDecaneConnect(config) {
4251
4411
  });
4252
4412
  deviceShare = recovered.deviceShare;
4253
4413
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4254
- if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4414
+ if (!deferDeviceProtection && !await storage2.getShare(userId) && isPasskeySupported()) {
4255
4415
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4256
4416
  }
4257
4417
  } else {
@@ -4272,7 +4432,7 @@ async function createDecaneConnect(config) {
4272
4432
  } catch (e) {
4273
4433
  if (e instanceof StaleDeviceShareError) {
4274
4434
  zeroize(deviceShare);
4275
- await storage.deleteShare(userId);
4435
+ await storage2.deleteShare(userId);
4276
4436
  if (stored) return returningUserUnlock(userId, jwt);
4277
4437
  } else {
4278
4438
  zeroize(deviceShare);
@@ -4296,7 +4456,7 @@ async function createDecaneConnect(config) {
4296
4456
  const result = await tee.rotateShares(jwt);
4297
4457
  try {
4298
4458
  await persistRotatedDeviceShare(userId, result.deviceShare);
4299
- const refreshed = await storage.getShare(userId);
4459
+ const refreshed = await storage2.getShare(userId);
4300
4460
  if (refreshed?.unlockMethod === "passkey" && refreshed.credentialId && refreshed.prfSalt) {
4301
4461
  await api.putRecoveryBackup(jwt, {
4302
4462
  wrappedDeviceShare: refreshed.wrappedDeviceShare,
@@ -4329,13 +4489,13 @@ async function createDecaneConnect(config) {
4329
4489
  }
4330
4490
  async function passkeyLoginCandidate() {
4331
4491
  if (!persisted?.userId || !isPasskeySupported()) return null;
4332
- return usablePasskeyShare(await storage.getShare(persisted.userId));
4492
+ return usablePasskeyShare(await storage2.getShare(persisted.userId));
4333
4493
  }
4334
4494
  async function passkeyDeviceCandidate() {
4335
4495
  if (!isPasskeySupported()) return null;
4336
4496
  if (persisted?.userId) return passkeyLoginCandidate();
4337
- if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
4338
- const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4497
+ if (lastUserId) return usablePasskeyShare(await storage2.getShare(lastUserId));
4498
+ const candidates = (await storage2.listShares()).map(usablePasskeyShare).filter(Boolean);
4339
4499
  return candidates.length === 1 ? candidates[0] : null;
4340
4500
  }
4341
4501
  async function canOfferPasskey() {
@@ -4343,15 +4503,15 @@ async function createDecaneConnect(config) {
4343
4503
  if (await passkeyDeviceCandidate()) return true;
4344
4504
  const known = persisted?.userId ?? lastUserId;
4345
4505
  if (!known) return false;
4346
- return await storage.getShare(known) == null;
4506
+ return await storage2.getShare(known) == null;
4347
4507
  }
4348
4508
  async function rememberProfile(userId, profile) {
4349
4509
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4350
4510
  if (!Object.keys(incoming).length) return;
4351
4511
  try {
4352
- const stored = await storage.getShare(userId);
4512
+ const stored = await storage2.getShare(userId);
4353
4513
  if (!stored) return;
4354
- await storage.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4514
+ await storage2.putShare({ ...stored, profile: { ...stored.profile, ...incoming } });
4355
4515
  } catch {
4356
4516
  }
4357
4517
  }
@@ -4375,7 +4535,7 @@ async function createDecaneConnect(config) {
4375
4535
  savePersisted(PERSIST_KEY, persisted);
4376
4536
  events.emit("connected");
4377
4537
  persistLiveSession();
4378
- const stored = await storage.getShare(userId);
4538
+ const stored = await storage2.getShare(userId);
4379
4539
  return {
4380
4540
  addresses,
4381
4541
  isNewUser: false,
@@ -4548,7 +4708,7 @@ async function createDecaneConnect(config) {
4548
4708
  events.emit("connected");
4549
4709
  persistLiveSession();
4550
4710
  scheduleAccessTokenExpiry(token, claims.expiresAt);
4551
- const stored = await storage.getShare(claims.userId);
4711
+ const stored = await storage2.getShare(claims.userId);
4552
4712
  return {
4553
4713
  addresses,
4554
4714
  isNewUser: false,
@@ -4578,7 +4738,7 @@ async function createDecaneConnect(config) {
4578
4738
  const candidate = await passkeyDeviceCandidate();
4579
4739
  if (candidate) return passkeyLogin(candidate);
4580
4740
  const known = persisted?.userId ?? lastUserId;
4581
- if (isPasskeySupported() && known && await storage.getShare(known) == null) {
4741
+ if (isPasskeySupported() && known && await storage2.getShare(known) == null) {
4582
4742
  return passkeyLoginWithoutShare(known);
4583
4743
  }
4584
4744
  throw new NewDeviceError();
@@ -4597,7 +4757,7 @@ async function createDecaneConnect(config) {
4597
4757
  async setUnlockPassword(password) {
4598
4758
  if (!auth) throw new ReconnectRequiredError();
4599
4759
  validateUnlockPassword(password);
4600
- const stored = await storage.getShare(auth.userId);
4760
+ const stored = await storage2.getShare(auth.userId);
4601
4761
  if (!stored) throw new NewDeviceError();
4602
4762
  const deviceShare = await aesGcmDecrypt(
4603
4763
  await getWrappingKey(stored),
@@ -4616,7 +4776,7 @@ async function createDecaneConnect(config) {
4616
4776
  );
4617
4777
  const iv = generateRandomBytes(12);
4618
4778
  const wrappedDeviceShare = await aesGcmEncrypt(shareKey, deviceShare, iv);
4619
- await storage.putShare({
4779
+ await storage2.putShare({
4620
4780
  ...stored,
4621
4781
  unlockMethod: "password",
4622
4782
  wrappedDeviceShare,
@@ -4696,9 +4856,9 @@ async function createDecaneConnect(config) {
4696
4856
  async removeUnlockPassword() {
4697
4857
  if (!auth) throw new ReconnectRequiredError();
4698
4858
  await api.revokeUnlockTokens(auth.jwt);
4699
- const stored = await storage.getShare(auth.userId);
4859
+ const stored = await storage2.getShare(auth.userId);
4700
4860
  if (!stored || stored.unlockMethod !== "password") return;
4701
- await storage.putShare({
4861
+ await storage2.putShare({
4702
4862
  ...stored,
4703
4863
  wrappedUnlockToken: void 0,
4704
4864
  unlockTokenIv: void 0
@@ -4706,13 +4866,13 @@ async function createDecaneConnect(config) {
4706
4866
  },
4707
4867
  async hasPasskey() {
4708
4868
  if (!auth) return false;
4709
- const stored = await storage.getShare(auth.userId);
4869
+ const stored = await storage2.getShare(auth.userId);
4710
4870
  return stored?.unlockMethod === "passkey";
4711
4871
  },
4712
4872
  async addPasskey() {
4713
4873
  if (!auth) throw new ReconnectRequiredError();
4714
4874
  if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
4715
- const stored = await storage.getShare(auth.userId);
4875
+ const stored = await storage2.getShare(auth.userId);
4716
4876
  if (stored?.unlockMethod === "passkey") return;
4717
4877
  if (!stored) {
4718
4878
  if (!serverAssistedRecovery) throw new NewDeviceError();
@@ -4745,11 +4905,11 @@ async function createDecaneConnect(config) {
4745
4905
  },
4746
4906
  async deviceProtected() {
4747
4907
  if (!auth) return false;
4748
- return await storage.getShare(auth.userId) !== null;
4908
+ return await storage2.getShare(auth.userId) !== null;
4749
4909
  },
4750
4910
  async protectDevice() {
4751
4911
  if (!auth) throw new ReconnectRequiredError();
4752
- if (await storage.getShare(auth.userId)) return true;
4912
+ if (await storage2.getShare(auth.userId)) return true;
4753
4913
  if (!serverAssistedRecovery) throw new NewDeviceError();
4754
4914
  const tee = new TeeClient(teeUrl);
4755
4915
  const provisioned = await tee.provisionDeviceShare(auth.jwt);
@@ -4759,7 +4919,7 @@ async function createDecaneConnect(config) {
4759
4919
  } finally {
4760
4920
  zeroize(provisioned);
4761
4921
  }
4762
- return await storage.getShare(auth.userId) !== null;
4922
+ return await storage2.getShare(auth.userId) !== null;
4763
4923
  },
4764
4924
  async unlock() {
4765
4925
  if (!auth) {
@@ -4801,8 +4961,7 @@ async function createDecaneConnect(config) {
4801
4961
  return bufferToB64u(sig);
4802
4962
  }
4803
4963
  if (req.chain.startsWith("tron:")) {
4804
- const msg = typeof req.message === "string" ? req.message : new TextDecoder().decode(req.message);
4805
- return tee.sign({ chain: req.chain, kind: "message", payload: msg }, auth?.jwt);
4964
+ return tee.sign({ chain: req.chain, kind: "message", payload: messageAsString(req.message) }, auth?.jwt);
4806
4965
  }
4807
4966
  throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
4808
4967
  },