@dynamic-labs-wallet/browser 1.0.43 → 1.0.44

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/index.esm.js CHANGED
@@ -1266,6 +1266,22 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
1266
1266
  }
1267
1267
  return false;
1268
1268
  };
1269
+ /**
1270
+ * Checks for the server's stale-share preflight rejection (HTTP 409 sent as
1271
+ * an SSE error event with `code: 'STALE_CLIENT_SHARES'`). The server compares
1272
+ * the `shareSetId` the SDK sends on sign requests against the wallet's active
1273
+ * share set and rejects BEFORE any room/ceremony work when they diverge
1274
+ * (e.g. the shares were rotated by a reshare/refresh on another device).
1275
+ * The SDK heals (re-syncs local shares from backup) and retries internally.
1276
+ *
1277
+ * The SSE event-stream layer copies the event's data fields onto the Error
1278
+ * object (see createErrorFromEventData), so `code` is available here.
1279
+ */ const isStaleClientSharesError = (error)=>{
1280
+ if (!error || typeof error !== 'object') {
1281
+ return false;
1282
+ }
1283
+ return error.code === 'STALE_CLIENT_SHARES';
1284
+ };
1269
1285
  /**
1270
1286
  * Determines whether a reshare operation should reuse the wallet's existing backup locations.
1271
1287
  *
@@ -2566,6 +2582,15 @@ const KNOWN_SHARE_SET_TYPES = new Set([
2566
2582
  'delegated',
2567
2583
  'server'
2568
2584
  ]);
2585
+ /**
2586
+ * keygenIds the server records for the wallet's Dynamic-backed-up client
2587
+ * shares — the reference generation that local shares are compared against.
2588
+ * Empty for first-time backups or legacy data without keygenIds.
2589
+ */ const recordedDynamicKeygenIds = (backupInfo)=>{
2590
+ var _backupInfo_backups;
2591
+ var _backupInfo_backups_BackupLocation_DYNAMIC;
2592
+ return ((_backupInfo_backups_BackupLocation_DYNAMIC = backupInfo == null ? void 0 : (_backupInfo_backups = backupInfo.backups) == null ? void 0 : _backupInfo_backups[BackupLocation.DYNAMIC]) != null ? _backupInfo_backups_BackupLocation_DYNAMIC : []).map((backup)=>backup.keygenId).filter((keygenId)=>typeof keygenId === 'string' && keygenId.length > 0);
2593
+ };
2569
2594
  class DynamicWalletClient {
2570
2595
  /**
2571
2596
  * Check if wallet has heavy operations in progress
@@ -3400,22 +3425,114 @@ class DynamicWalletClient {
3400
3425
  }
3401
3426
  }
3402
3427
  /**
3403
- * On public-key mismatch, restore local shares from backup and rethrow so
3404
- * the caller's next call (with refreshed shares) succeeds. Auto-retry was
3405
- * removed because in the iframe transport the error response is already
3406
- * posted to the host before any retry can resolve.
3407
- * Passthrough for non-mismatch errors.
3408
- */ async recoverKeySharesOnMismatch({ error, accountAddress, password, signedSessionId, walletOperation, mfaToken, traceContext, dynamicRequestId }) {
3409
- if (!isPublicKeyMismatchError(error)) {
3410
- throw error;
3428
+ * Best-effort heal for stale local shares. Runs verifyAndRecoverStaleShare
3429
+ * through the shared recovery queue: concurrent triggers join the in-flight
3430
+ * recovery (WalletQueueManager dedup), and execution is deadlock-safe when
3431
+ * called from inside a queued sign/heavy operation. Never throws — callers
3432
+ * are already on a failing path and rethrow their original error.
3433
+ */ async healStaleShares(params) {
3434
+ const runCheck = ()=>WalletQueueManager.queueRecoverOperation(params.accountAddress, WalletOperation.RECOVER, ()=>this.verifyAndRecoverStaleShare(params));
3435
+ try {
3436
+ const result = await runCheck();
3437
+ // The queue dedup returns the in-flight recovery's promise, dropping
3438
+ // this caller's params. If that run skipped recovery but we carry a
3439
+ // confirmed mismatch, re-run once with our params. (A run we started
3440
+ // ourselves with confirmedMismatch=true always recovers, so this only
3441
+ // fires after joining a weaker concurrent check.)
3442
+ if (params.confirmedMismatch && (result == null ? void 0 : result.recovered) === false) {
3443
+ await runCheck();
3444
+ }
3445
+ } catch (error) {
3446
+ this.logger.error('[StaleShareCheck] share recovery failed', {
3447
+ accountAddress: params.accountAddress,
3448
+ dynamicRequestId: params.dynamicRequestId,
3449
+ trigger: params.trigger,
3450
+ error: error instanceof Error ? error.message : String(error)
3451
+ });
3411
3452
  }
3412
- this.logger.info('[DynamicWaasWalletClient] Public key mismatch detected, recovering key shares from backup', _extends({
3453
+ }
3454
+ /**
3455
+ * Ground-truth staleness check: refreshes the wallet from the API, compares
3456
+ * local share keygenIds against the server's recorded Dynamic-backup
3457
+ * keygenIds, and restores from backup when the local generation is stale
3458
+ * (a reshare/refresh on another device/browser rotates the shares without
3459
+ * this client noticing). No-ops when shares are fresh, so callers may run
3460
+ * it on any ceremony failure regardless of the error message.
3461
+ */ async verifyAndRecoverStaleShare({ accountAddress, chainName, password, signedSessionId, walletOperation, mfaToken, dynamicRequestId, traceContext, bitcoinConfig, trigger, confirmedMismatch }) {
3462
+ this.logger.info('[StaleShareCheck] verifying local share generation against server', _extends({
3413
3463
  accountAddress,
3414
- walletOperation,
3464
+ chainName,
3465
+ trigger,
3466
+ confirmedMismatch,
3415
3467
  dynamicRequestId
3416
3468
  }, this.getTraceContext(traceContext)));
3417
- // Refresh walletMap from API before recovery to ensure backup info is up-to-date.
3469
+ // Refresh walletMap from the API: the cached entry may predate a rotation
3470
+ // performed on another device, which would make this comparison a no-op.
3471
+ // Full getWallets (not just backup info) so shareSetId etc. are also
3472
+ // current for the caller's retry.
3418
3473
  await this.getWallets();
3474
+ const wallet = this.getWalletFromMap(accountAddress);
3475
+ const recordedKeygenIds = recordedDynamicKeygenIds(wallet == null ? void 0 : wallet.clientKeySharesBackupInfo);
3476
+ let staleness;
3477
+ let localKeygenIds = [];
3478
+ if (recordedKeygenIds.length === 0) {
3479
+ // First-time backup or legacy data (server didn't populate keygenId) —
3480
+ // generation can't be compared. A confirmed mismatch still recovers
3481
+ // below (legacy recovery behavior); an unclassified
3482
+ // failure does not.
3483
+ staleness = 'unverifiable';
3484
+ } else {
3485
+ try {
3486
+ const localShares = await this.getClientKeySharesFromStorage({
3487
+ accountAddress
3488
+ });
3489
+ const comparison = await this.compareShareGenerations({
3490
+ accountAddress,
3491
+ chainName,
3492
+ localShares,
3493
+ recordedKeygenIds,
3494
+ bitcoinConfig
3495
+ });
3496
+ localKeygenIds = comparison.localKeygenIds;
3497
+ staleness = comparison.fresh ? 'fresh' : 'stale';
3498
+ } catch (error) {
3499
+ // Unreadable/corrupted local share — treat as stale and recover.
3500
+ this.logger.warn('[StaleShareCheck] could not derive keygenId from local shares — treating as stale', {
3501
+ accountAddress,
3502
+ trigger,
3503
+ dynamicRequestId,
3504
+ error: error instanceof Error ? error.message : String(error)
3505
+ });
3506
+ staleness = 'stale';
3507
+ }
3508
+ }
3509
+ // Stale generation always recovers. A confirmed mismatch also recovers
3510
+ // when the comparison is inconclusive or even reports fresh (possible
3511
+ // server-side inconsistency) — matching the legacy recovery behavior.
3512
+ const shouldRecover = staleness === 'stale' || confirmedMismatch;
3513
+ if (!shouldRecover) {
3514
+ this.logger.info('[StaleShareCheck] no recovery needed', {
3515
+ accountAddress,
3516
+ trigger,
3517
+ staleness,
3518
+ localKeygenIds,
3519
+ dynamicRequestId
3520
+ });
3521
+ return {
3522
+ recovered: false,
3523
+ localKeygenIds,
3524
+ recordedKeygenIds
3525
+ };
3526
+ }
3527
+ this.logger.warn('[StaleShareCheck] recovering current shares from backup', _extends({
3528
+ accountAddress,
3529
+ trigger,
3530
+ confirmedMismatch,
3531
+ staleness,
3532
+ localKeygenIds,
3533
+ recordedKeygenIds,
3534
+ dynamicRequestId
3535
+ }, this.getTraceContext(traceContext)));
3419
3536
  await this.internalRecoverEncryptedBackupByWallet({
3420
3537
  accountAddress,
3421
3538
  password,
@@ -3424,12 +3541,35 @@ class DynamicWalletClient {
3424
3541
  mfaToken,
3425
3542
  storeRecoveredShares: true
3426
3543
  });
3427
- this.logger.info('[DynamicWaasWalletClient] Key shares recovered; rethrowing original error for caller to retry', _extends({
3544
+ this.logger.info('[StaleShareCheck] recovery completed next operation will use the recovered shares', {
3428
3545
  accountAddress,
3429
- walletOperation,
3546
+ trigger,
3430
3547
  dynamicRequestId
3431
- }, this.getTraceContext(traceContext)));
3432
- throw error;
3548
+ });
3549
+ return {
3550
+ recovered: true,
3551
+ localKeygenIds,
3552
+ recordedKeygenIds
3553
+ };
3554
+ }
3555
+ /**
3556
+ * Set-compare local share keygenIds (MPC exportID) against the keygenIds
3557
+ * the server recorded for the wallet's Dynamic backups. Shared by the
3558
+ * stale-share heal (verifyAndRecoverStaleShare) and the backup preflight
3559
+ * (ensureLocalSharesAreFresh).
3560
+ */ async compareShareGenerations({ accountAddress, chainName, localShares, recordedKeygenIds, bitcoinConfig }) {
3561
+ const mpcSigner = getMPCSigner({
3562
+ chainName,
3563
+ baseRelayUrl: this.baseMPCRelayApiUrl,
3564
+ bitcoinConfig: bitcoinConfig != null ? bitcoinConfig : this.getBitcoinConfigForChain(chainName, accountAddress)
3565
+ });
3566
+ const localKeygenIds = await Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)));
3567
+ const recordedSet = new Set(recordedKeygenIds);
3568
+ const fresh = localKeygenIds.length === recordedSet.size && localKeygenIds.every((id)=>recordedSet.has(id));
3569
+ return {
3570
+ fresh,
3571
+ localKeygenIds
3572
+ };
3433
3573
  }
3434
3574
  //todo: need to modify with imported flag
3435
3575
  async sign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig }) {
@@ -3448,7 +3588,7 @@ class DynamicWalletClient {
3448
3588
  bitcoinConfig
3449
3589
  }));
3450
3590
  }
3451
- async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig }) {
3591
+ async internalSign({ accountAddress, message, chainName, password = undefined, isFormatted = false, signedSessionId, mfaToken, elevatedAccessToken, context, onError, traceContext, bitcoinConfig, staleShareRetry = false }) {
3452
3592
  const dynamicRequestId = v4();
3453
3593
  try {
3454
3594
  await this.verifyPassword({
@@ -3491,6 +3631,17 @@ class DynamicWalletClient {
3491
3631
  // (sseErrorPromise would never reach Promise.race in that path)
3492
3632
  // eslint-disable-next-line @typescript-eslint/no-empty-function
3493
3633
  sseErrorPromise.catch(()=>{});
3634
+ // Dedicated channel for the server's 409 stale-shares preflight: it must
3635
+ // abort the ceremony into the heal-and-retry path WITHOUT settling the
3636
+ // host request (so the transparent retry's outcome is what the host
3637
+ // sees). Needed as a race arm because in the cached-room flow
3638
+ // serverSignPromise is never awaited, so its rejection alone would float.
3639
+ let rejectOnStaleShares;
3640
+ const staleSharesPromise = new Promise((_, reject)=>{
3641
+ rejectOnStaleShares = reject;
3642
+ });
3643
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3644
+ staleSharesPromise.catch(()=>{});
3494
3645
  // Perform the server sign
3495
3646
  const serverSignPromise = this.serverSign({
3496
3647
  walletId: wallet.walletId,
@@ -3502,6 +3653,13 @@ class DynamicWalletClient {
3502
3653
  roomId,
3503
3654
  context,
3504
3655
  onError: (error)=>{
3656
+ // The server's 409 stale-shares preflight rejection is healed and
3657
+ // retried internally (see the catch below) — don't settle the host
3658
+ // request or poison the SSE race with it.
3659
+ if (isStaleClientSharesError(error)) {
3660
+ rejectOnStaleShares(error);
3661
+ return;
3662
+ }
3505
3663
  onError == null ? void 0 : onError(error);
3506
3664
  rejectOnSSEError(error);
3507
3665
  },
@@ -3509,6 +3667,11 @@ class DynamicWalletClient {
3509
3667
  traceContext,
3510
3668
  bitcoinConfig
3511
3669
  });
3670
+ // In the cached-room flow the promise below is never awaited — attach a
3671
+ // handler so a rejection (e.g. the 409 preflight, which also arrives via
3672
+ // staleSharesPromise above) doesn't surface as an unhandled rejection.
3673
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
3674
+ serverSignPromise.catch(()=>{});
3512
3675
  // Only await if roomId is not provided
3513
3676
  roomId = roomId != null ? roomId : (await serverSignPromise).roomId;
3514
3677
  this.logger.info('[DynamicWaasWalletClient] Server sign completed', _extends({
@@ -3521,6 +3684,9 @@ class DynamicWalletClient {
3521
3684
  operation: 'serverSign'
3522
3685
  }, this.getTraceContext(traceContext)));
3523
3686
  let signature;
3687
+ // Set by the clientSign rejection handler below; read in the catch to
3688
+ // detect a client-side mismatch masked by a generic SSE race winner.
3689
+ let clientSignMismatchError;
3524
3690
  try {
3525
3691
  // Race clientSign against SSE errors so that server-side failures
3526
3692
  // (e.g. policy violations) immediately reject instead of hanging
@@ -3548,6 +3714,12 @@ class DynamicWalletClient {
3548
3714
  roomId: undefined,
3549
3715
  context,
3550
3716
  onError: (error)=>{
3717
+ // See the initial serverSign onError: 409 stale-shares is
3718
+ // healed + retried internally, never surfaced to the host.
3719
+ if (isStaleClientSharesError(error)) {
3720
+ rejectOnStaleShares(error);
3721
+ return;
3722
+ }
3551
3723
  onError == null ? void 0 : onError(error);
3552
3724
  rejectOnSSEError(error);
3553
3725
  },
@@ -3558,26 +3730,63 @@ class DynamicWalletClient {
3558
3730
  return freshRoomId;
3559
3731
  }
3560
3732
  });
3561
- // Prevent unhandled rejection from the losing promise in the race:
3562
- // if sseErrorPromise wins, clientSign keeps running and will eventually
3563
- // reject (MPC timeout); if clientSign wins, sseErrorPromise may never settle.
3564
- // eslint-disable-next-line @typescript-eslint/no-empty-function
3565
- clientSignPromise.catch(()=>{});
3733
+ // Handle the losing race promise (prevents an unhandled rejection) and
3734
+ // capture a client-side ERROR_PUBLIC_KEY_MISMATCH here the only place
3735
+ // a stale-local-share failure surfaces, since the server's generic SSE
3736
+ // error wins the race and masks it. Schedule recovery regardless of
3737
+ // which promise wins or when this rejection settles.
3738
+ clientSignPromise.catch((error)=>{
3739
+ if (isPublicKeyMismatchError(error)) {
3740
+ clientSignMismatchError = error;
3741
+ this.logger.warn('[StaleShareCheck] clientSign rejected with public-key mismatch — scheduling share recovery', _extends({
3742
+ accountAddress,
3743
+ chainName,
3744
+ dynamicRequestId
3745
+ }, this.getTraceContext(traceContext)));
3746
+ void this.healStaleShares({
3747
+ accountAddress,
3748
+ chainName,
3749
+ password,
3750
+ signedSessionId,
3751
+ walletOperation: WalletOperation.SIGN_MESSAGE,
3752
+ mfaToken,
3753
+ dynamicRequestId,
3754
+ traceContext,
3755
+ bitcoinConfig,
3756
+ trigger: 'clientSign rejection',
3757
+ confirmedMismatch: true
3758
+ });
3759
+ }
3760
+ });
3566
3761
  signature = await Promise.race([
3567
3762
  clientSignPromise,
3568
- sseErrorPromise
3763
+ sseErrorPromise,
3764
+ staleSharesPromise
3569
3765
  ]);
3570
3766
  } catch (signError) {
3571
- // Restore shares from backup on mismatch; passthrough otherwise.
3572
- await this.recoverKeySharesOnMismatch({
3573
- error: signError,
3767
+ // The 409 stale-shares preflight is handled exclusively by the outer
3768
+ // catch (heal + transparent retry) — skip the generic heal here so the
3769
+ // ground-truth check runs once, right before the retry decision.
3770
+ if (isStaleClientSharesError(signError)) {
3771
+ throw signError;
3772
+ }
3773
+ // Heal before rethrowing so the caller's retry succeeds. Run the
3774
+ // ground-truth check whatever the raced error was: a generic SSE
3775
+ // error frequently wins the race while the client-side mismatch
3776
+ // settles later, so error-string inspection alone is timing-dependent.
3777
+ // The check no-ops when shares are fresh.
3778
+ await this.healStaleShares({
3574
3779
  accountAddress,
3780
+ chainName,
3575
3781
  password,
3576
3782
  signedSessionId,
3577
3783
  walletOperation: WalletOperation.SIGN_MESSAGE,
3578
3784
  mfaToken,
3785
+ dynamicRequestId,
3579
3786
  traceContext,
3580
- dynamicRequestId
3787
+ bitcoinConfig,
3788
+ trigger: 'sign failure',
3789
+ confirmedMismatch: isPublicKeyMismatchError(signError) || clientSignMismatchError !== undefined
3581
3790
  });
3582
3791
  throw signError;
3583
3792
  }
@@ -3593,6 +3802,51 @@ class DynamicWalletClient {
3593
3802
  }, this.getTraceContext(traceContext)));
3594
3803
  return signature;
3595
3804
  } catch (error) {
3805
+ // Server-side stale-share preflight (409 STALE_CLIENT_SHARES): the
3806
+ // request was rejected BEFORE any room/ceremony work, and the onError
3807
+ // wrappers above kept it away from the host — so the host request is
3808
+ // still pending. Heal the local shares from backup and transparently
3809
+ // retry once; the host only ever sees the (successful) outcome.
3810
+ if (!staleShareRetry && isStaleClientSharesError(error)) {
3811
+ // Heal first (re-sync local shares + walletMap from the server), then
3812
+ // retry regardless of whether shares were replaced: even a "fresh"
3813
+ // verdict means the heal's getWallets() refreshed the stale
3814
+ // walletMap.shareSetId the server rejected. The retry is guarded to
3815
+ // run at most once; if it 409s again the error surfaces normally.
3816
+ await this.healStaleShares({
3817
+ accountAddress,
3818
+ chainName,
3819
+ password,
3820
+ signedSessionId,
3821
+ walletOperation: WalletOperation.SIGN_MESSAGE,
3822
+ mfaToken,
3823
+ dynamicRequestId,
3824
+ traceContext,
3825
+ bitcoinConfig,
3826
+ trigger: 'server 409 stale-shares preflight',
3827
+ confirmedMismatch: true
3828
+ });
3829
+ this.logger.info('[StaleShareCheck] retrying sign transparently after 409 stale-shares preflight', _extends({
3830
+ accountAddress,
3831
+ chainName,
3832
+ dynamicRequestId
3833
+ }, this.getTraceContext(traceContext)));
3834
+ return await this.internalSign({
3835
+ accountAddress,
3836
+ message,
3837
+ chainName,
3838
+ password,
3839
+ isFormatted,
3840
+ signedSessionId,
3841
+ mfaToken,
3842
+ elevatedAccessToken,
3843
+ context,
3844
+ onError,
3845
+ traceContext,
3846
+ bitcoinConfig,
3847
+ staleShareRetry: true
3848
+ });
3849
+ }
3596
3850
  logError({
3597
3851
  message: 'Error in sign',
3598
3852
  error: error,
@@ -3606,12 +3860,13 @@ class DynamicWalletClient {
3606
3860
  throw error;
3607
3861
  }
3608
3862
  }
3609
- async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
3863
+ async refreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, sessionPublicKey, mfaToken, elevatedAccessToken, traceContext }) {
3610
3864
  return WalletQueueManager.queueHeavyOperation(accountAddress, WalletOperation.REFRESH, ()=>this.internalRefreshWalletAccountShares({
3611
3865
  accountAddress,
3612
3866
  chainName,
3613
3867
  password,
3614
3868
  signedSessionId,
3869
+ sessionPublicKey,
3615
3870
  mfaToken,
3616
3871
  elevatedAccessToken,
3617
3872
  traceContext
@@ -3678,7 +3933,7 @@ class DynamicWalletClient {
3678
3933
  addressType: addressType
3679
3934
  } : undefined;
3680
3935
  }
3681
- async internalRefreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
3936
+ async internalRefreshWalletAccountShares({ accountAddress, chainName, password = undefined, signedSessionId, sessionPublicKey, mfaToken, elevatedAccessToken, traceContext, staleShareRetry = false }) {
3682
3937
  const dynamicRequestId = v4();
3683
3938
  try {
3684
3939
  this.assertPasswordRequired(password);
@@ -3770,16 +4025,19 @@ class DynamicWalletClient {
3770
4025
  try {
3771
4026
  refreshResults = await Promise.all(clientKeyShares.map((clientKeyShare)=>mpcSigner.refresh(roomId, clientKeyShare)));
3772
4027
  } catch (refreshError) {
3773
- // Restore shares from backup on mismatch; passthrough otherwise.
3774
- await this.recoverKeySharesOnMismatch({
3775
- error: refreshError,
4028
+ // Heal stale local shares before rethrowing so a retry succeeds.
4029
+ await this.healStaleShares({
3776
4030
  accountAddress,
4031
+ chainName,
3777
4032
  password,
3778
4033
  signedSessionId,
3779
4034
  walletOperation: WalletOperation.REFRESH,
3780
4035
  mfaToken,
4036
+ dynamicRequestId,
3781
4037
  traceContext,
3782
- dynamicRequestId
4038
+ bitcoinConfig,
4039
+ trigger: 'refresh failure',
4040
+ confirmedMismatch: isPublicKeyMismatchError(refreshError)
3783
4041
  });
3784
4042
  throw refreshError;
3785
4043
  }
@@ -3805,7 +4063,8 @@ class DynamicWalletClient {
3805
4063
  accountAddress,
3806
4064
  clientKeyShares: refreshResults,
3807
4065
  password: password != null ? password : this.environmentId,
3808
- signedSessionId
4066
+ signedSessionId,
4067
+ sessionPublicKey
3809
4068
  })
3810
4069
  });
3811
4070
  await this.setClientKeySharesToStorage({
@@ -3831,6 +4090,42 @@ class DynamicWalletClient {
3831
4090
  });
3832
4091
  throw error;
3833
4092
  }
4093
+ // Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
4094
+ // BEFORE any room/ceremony work, so nothing was rotated. Heal the local
4095
+ // shares from backup and transparently retry once — even a "fresh" heal
4096
+ // verdict matters, because its getWallets() refreshed the stale
4097
+ // walletMap.shareSetId the server rejected.
4098
+ if (!staleShareRetry && isStaleClientSharesError(error)) {
4099
+ await this.healStaleShares({
4100
+ accountAddress,
4101
+ chainName,
4102
+ password,
4103
+ signedSessionId,
4104
+ walletOperation: WalletOperation.REFRESH,
4105
+ mfaToken,
4106
+ dynamicRequestId,
4107
+ traceContext,
4108
+ bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress),
4109
+ trigger: 'server 409 stale-shares preflight (refresh)',
4110
+ confirmedMismatch: true
4111
+ });
4112
+ this.logger.info('[StaleShareCheck] retrying refresh transparently after 409 stale-shares preflight', _extends({
4113
+ accountAddress,
4114
+ chainName,
4115
+ dynamicRequestId
4116
+ }, this.getTraceContext(traceContext)));
4117
+ return await this.internalRefreshWalletAccountShares({
4118
+ accountAddress,
4119
+ chainName,
4120
+ password,
4121
+ signedSessionId,
4122
+ sessionPublicKey,
4123
+ mfaToken,
4124
+ elevatedAccessToken,
4125
+ traceContext,
4126
+ staleShareRetry: true
4127
+ });
4128
+ }
3834
4129
  logError({
3835
4130
  message: 'Error in refreshWalletAccountShares',
3836
4131
  error: error,
@@ -3922,7 +4217,7 @@ class DynamicWalletClient {
3922
4217
  existingClientKeyShares
3923
4218
  };
3924
4219
  }
3925
- async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
4220
+ async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
3926
4221
  return WalletQueueManager.queueHeavyOperation(accountAddress, WalletOperation.RESHARE, ()=>this.internalReshare({
3927
4222
  chainName,
3928
4223
  accountAddress,
@@ -3930,6 +4225,7 @@ class DynamicWalletClient {
3930
4225
  newThresholdSignatureScheme,
3931
4226
  password,
3932
4227
  signedSessionId,
4228
+ sessionPublicKey,
3933
4229
  cloudProviders,
3934
4230
  delegateToProjectEnvironment,
3935
4231
  mfaToken,
@@ -4414,7 +4710,7 @@ class DynamicWalletClient {
4414
4710
  clientKeySharesBackupInfo: updatedBackupInfo
4415
4711
  } : {}));
4416
4712
  }
4417
- async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource }) {
4713
+ async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false }) {
4418
4714
  const dynamicRequestId = v4();
4419
4715
  // Password validation - wrapped in try-catch for consistent error handling
4420
4716
  // This path should NOT wipe key shares on failure (shares remain valid)
@@ -4626,15 +4922,18 @@ class DynamicWalletClient {
4626
4922
  existingReshareResults = existingSettled.map((r)=>r.value);
4627
4923
  newReshareResults = newSettled.map((r)=>r.value);
4628
4924
  } catch (reshareError) {
4629
- // Restore shares from backup on mismatch; passthrough otherwise.
4630
- await this.recoverKeySharesOnMismatch({
4631
- error: reshareError,
4925
+ // Heal stale local shares before rethrowing so a retry succeeds.
4926
+ await this.healStaleShares({
4632
4927
  accountAddress,
4928
+ chainName,
4633
4929
  password,
4634
4930
  signedSessionId,
4635
4931
  walletOperation: WalletOperation.RESHARE,
4636
4932
  mfaToken,
4637
- dynamicRequestId
4933
+ dynamicRequestId,
4934
+ bitcoinConfig,
4935
+ trigger: 'reshare failure',
4936
+ confirmedMismatch: isPublicKeyMismatchError(reshareError)
4638
4937
  });
4639
4938
  throw reshareError;
4640
4939
  }
@@ -4669,6 +4968,7 @@ class DynamicWalletClient {
4669
4968
  accountAddress,
4670
4969
  password,
4671
4970
  signedSessionId,
4971
+ sessionPublicKey,
4672
4972
  distribution,
4673
4973
  googleDriveAccessToken,
4674
4974
  googleDriveTokenSource
@@ -4706,6 +5006,47 @@ class DynamicWalletClient {
4706
5006
  }
4707
5007
  });
4708
5008
  } catch (error) {
5009
+ // Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
5010
+ // BEFORE any room/ceremony work, so local shares are intact (nothing to
5011
+ // wipe). Heal from backup and transparently retry once — even a "fresh"
5012
+ // heal verdict matters, because its getWallets() refreshed the stale
5013
+ // walletMap.shareSetId the server rejected.
5014
+ if (!staleShareRetry && isStaleClientSharesError(error)) {
5015
+ await this.healStaleShares({
5016
+ accountAddress,
5017
+ chainName,
5018
+ password,
5019
+ signedSessionId,
5020
+ walletOperation: WalletOperation.RESHARE,
5021
+ mfaToken,
5022
+ dynamicRequestId,
5023
+ bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress),
5024
+ trigger: 'server 409 stale-shares preflight (reshare)',
5025
+ confirmedMismatch: true
5026
+ });
5027
+ this.logger.info('[StaleShareCheck] retrying reshare transparently after 409 stale-shares preflight', {
5028
+ accountAddress,
5029
+ chainName,
5030
+ dynamicRequestId
5031
+ });
5032
+ return await this.internalReshare({
5033
+ chainName,
5034
+ accountAddress,
5035
+ oldThresholdSignatureScheme,
5036
+ newThresholdSignatureScheme,
5037
+ password,
5038
+ signedSessionId,
5039
+ sessionPublicKey,
5040
+ cloudProviders,
5041
+ delegateToProjectEnvironment,
5042
+ mfaToken,
5043
+ elevatedAccessToken,
5044
+ revokeDelegation,
5045
+ googleDriveAccessToken,
5046
+ googleDriveTokenSource,
5047
+ staleShareRetry: true
5048
+ });
5049
+ }
4709
5050
  logError({
4710
5051
  message: 'Error in reshare, resetting wallet to previous state',
4711
5052
  error: error,
@@ -4720,9 +5061,12 @@ class DynamicWalletClient {
4720
5061
  dynamicRequestId
4721
5062
  }
4722
5063
  });
4723
- // Skip the wipe on mismatch — recoverKeySharesOnMismatch just restored
4724
- // these shares from backup, undoing that would break the caller's retry.
4725
- if (!isPublicKeyMismatchError(error)) {
5064
+ // Skip the wipe on mismatch — healStaleShares just restored these
5065
+ // shares from backup, undoing that would break the caller's retry.
5066
+ // Same for a 409 stale-shares preflight (only reachable here on the
5067
+ // retried attempt): the request was rejected before any ceremony, so
5068
+ // local shares are consistent — wiping would strand the user.
5069
+ if (!isPublicKeyMismatchError(error) && !isStaleClientSharesError(error)) {
4726
5070
  this.updateWalletMap(accountAddress, {
4727
5071
  thresholdSignatureScheme: oldThresholdSignatureScheme
4728
5072
  });
@@ -4734,7 +5078,7 @@ class DynamicWalletClient {
4734
5078
  throw error;
4735
5079
  }
4736
5080
  }
4737
- async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
5081
+ async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
4738
5082
  try {
4739
5083
  const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
4740
5084
  if (!delegateToProjectEnvironment) {
@@ -4763,6 +5107,7 @@ class DynamicWalletClient {
4763
5107
  newThresholdSignatureScheme,
4764
5108
  password,
4765
5109
  signedSessionId,
5110
+ sessionPublicKey,
4766
5111
  cloudProviders: activeProviders,
4767
5112
  // Server soft-deletes the wallet API key on revoke; a delegated
4768
5113
  // distribution would 400 on the follow-up delivery POST.
@@ -4781,7 +5126,7 @@ class DynamicWalletClient {
4781
5126
  throw error;
4782
5127
  }
4783
5128
  }
4784
- async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
5129
+ async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken }) {
4785
5130
  var _this_featureFlags;
4786
5131
  const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
4787
5132
  if (useShareSetReshare) {
@@ -4806,6 +5151,7 @@ class DynamicWalletClient {
4806
5151
  accountAddress,
4807
5152
  password,
4808
5153
  signedSessionId,
5154
+ sessionPublicKey,
4809
5155
  mfaToken,
4810
5156
  // FF on: reshare to 2/2 — the new delegated share set is always 2/2
4811
5157
  // (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
@@ -4820,7 +5166,7 @@ class DynamicWalletClient {
4820
5166
  const delegatedKeyShares = backupInfo.backups[BackupLocation.DELEGATED] || [];
4821
5167
  return delegatedKeyShares;
4822
5168
  }
4823
- async revokeDelegation({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
5169
+ async revokeDelegation({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken }) {
4824
5170
  var _this_featureFlags;
4825
5171
  const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
4826
5172
  if (useShareSetReshare) {
@@ -4866,6 +5212,7 @@ class DynamicWalletClient {
4866
5212
  accountAddress,
4867
5213
  password,
4868
5214
  signedSessionId,
5215
+ sessionPublicKey,
4869
5216
  mfaToken,
4870
5217
  newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
4871
5218
  revokeDelegation: true,
@@ -4882,6 +5229,22 @@ class DynamicWalletClient {
4882
5229
  return new BIP340KeygenResult(extractedPubkey, secretShare);
4883
5230
  }
4884
5231
  async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
5232
+ // Public entry point delegates to the private implementation. Keeping the
5233
+ // `staleShareRetry` flag off the public signature mirrors sign/refresh/
5234
+ // reshare and ensures the transparent 409 retry can't be suppressed by
5235
+ // external callers or intercepted by subclass overrides.
5236
+ return this.internalExportKey({
5237
+ accountAddress,
5238
+ chainName,
5239
+ bitcoinConfig,
5240
+ password,
5241
+ signedSessionId,
5242
+ mfaToken,
5243
+ elevatedAccessToken,
5244
+ traceContext
5245
+ });
5246
+ }
5247
+ async internalExportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext, staleShareRetry = false }) {
4885
5248
  const dynamicRequestId = v4();
4886
5249
  try {
4887
5250
  const wallet = await this.getWallet({
@@ -4926,10 +5289,51 @@ class DynamicWalletClient {
4926
5289
  roomId: data.roomId
4927
5290
  }, this.getTraceContext(traceContext)));
4928
5291
  const derivedPrivateKey = await this.derivePrivateKeyFromExport(mpcSigner, keyExportRaw, wallet);
5292
+ // derivePrivateKeyFromExport returns undefined for unsupported signer
5293
+ // types; fail loudly rather than returning undefined as a string.
5294
+ if (derivedPrivateKey === undefined) {
5295
+ throw new Error(`Private key export is not supported for chain ${chainName}`);
5296
+ }
4929
5297
  return {
4930
5298
  derivedPrivateKey
4931
5299
  };
4932
5300
  } catch (error) {
5301
+ // Server-side stale-share preflight (409 STALE_CLIENT_SHARES): rejected
5302
+ // BEFORE any room/ceremony work. Heal the local shares from backup and
5303
+ // transparently retry once — even a "fresh" heal verdict matters,
5304
+ // because its getWallets() refreshed the stale walletMap.shareSetId the
5305
+ // server rejected.
5306
+ if (!staleShareRetry && isStaleClientSharesError(error)) {
5307
+ await this.healStaleShares({
5308
+ accountAddress,
5309
+ chainName,
5310
+ password,
5311
+ signedSessionId,
5312
+ walletOperation: WalletOperation.EXPORT_PRIVATE_KEY,
5313
+ mfaToken,
5314
+ dynamicRequestId,
5315
+ traceContext,
5316
+ bitcoinConfig,
5317
+ trigger: 'server 409 stale-shares preflight (export)',
5318
+ confirmedMismatch: true
5319
+ });
5320
+ this.logger.info('[StaleShareCheck] retrying export transparently after 409 stale-shares preflight', _extends({
5321
+ accountAddress,
5322
+ chainName,
5323
+ dynamicRequestId
5324
+ }, this.getTraceContext(traceContext)));
5325
+ return await this.internalExportKey({
5326
+ accountAddress,
5327
+ chainName,
5328
+ bitcoinConfig,
5329
+ password,
5330
+ signedSessionId,
5331
+ mfaToken,
5332
+ elevatedAccessToken,
5333
+ traceContext,
5334
+ staleShareRetry: true
5335
+ });
5336
+ }
4933
5337
  logError({
4934
5338
  message: 'Error in exportKey',
4935
5339
  error: error,
@@ -4939,6 +5343,22 @@ class DynamicWalletClient {
4939
5343
  dynamicRequestId
4940
5344
  }, this.getTraceContext(traceContext))
4941
5345
  });
5346
+ // Heal stale local shares before rethrowing so a retry succeeds —
5347
+ // export reconstructs from the local share, so a generation rotated on
5348
+ // another device fails here too.
5349
+ await this.healStaleShares({
5350
+ accountAddress,
5351
+ chainName,
5352
+ password,
5353
+ signedSessionId,
5354
+ walletOperation: WalletOperation.EXPORT_PRIVATE_KEY,
5355
+ mfaToken,
5356
+ dynamicRequestId,
5357
+ traceContext,
5358
+ bitcoinConfig,
5359
+ trigger: 'export failure',
5360
+ confirmedMismatch: isPublicKeyMismatchError(error)
5361
+ });
4942
5362
  throw error;
4943
5363
  }
4944
5364
  }
@@ -5308,6 +5728,14 @@ class DynamicWalletClient {
5308
5728
  * Auto-recovery logic has been removed in favor of the queue pattern.
5309
5729
  * Callers should handle recovery explicitly if needed.
5310
5730
  */ async ensureClientShare(accountAddress) {
5731
+ var _WalletQueueManager_getPendingRecoveryPromise;
5732
+ // A recovery may be in flight (e.g. a stale-share heal scheduled when a
5733
+ // previous sign hit a public-key mismatch). Wait for it so this operation
5734
+ // reads the recovered shares instead of the superseded ones. Recovery
5735
+ // failures are logged where they happen; this operation should proceed
5736
+ // with whatever shares exist rather than fail on the recovery.
5737
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
5738
+ await ((_WalletQueueManager_getPendingRecoveryPromise = WalletQueueManager.getPendingRecoveryPromise(accountAddress)) == null ? void 0 : _WalletQueueManager_getPendingRecoveryPromise.catch(()=>{}));
5311
5739
  const shares = await this.getClientKeySharesFromStorage({
5312
5740
  accountAddress
5313
5741
  });
@@ -5316,13 +5744,14 @@ class DynamicWalletClient {
5316
5744
  }
5317
5745
  return shares;
5318
5746
  }
5319
- async backupToDynamicBackend({ walletId, clientShares, password, signedSessionId, dynamicRequestId, chainName, bitcoinConfig, isPasswordEncrypted, accountAddress, preEncryptedShares, passwordUpdateBatchId, encryptionVersion = DEFAULT_ENCRYPTION_VERSION }) {
5747
+ async backupToDynamicBackend({ walletId, clientShares, password, signedSessionId, sessionPublicKey, dynamicRequestId, chainName, bitcoinConfig, isPasswordEncrypted, accountAddress, preEncryptedShares, passwordUpdateBatchId, encryptionVersion = DEFAULT_ENCRYPTION_VERSION }) {
5320
5748
  var _this_getWalletFromMap;
5321
5749
  const encryptedDynamicShares = preEncryptedShares != null ? preEncryptedShares : await Promise.all(clientShares.map((keyShare)=>this.encryptKeyShare({
5322
5750
  keyShare,
5323
5751
  password,
5324
5752
  version: encryptionVersion
5325
5753
  })));
5754
+ const resolvedSessionPublicKey = sessionPublicKey != null ? sessionPublicKey : await (this.getSessionPublicKeyCallback == null ? void 0 : this.getSessionPublicKeyCallback.call(this));
5326
5755
  const data = await this.apiClient.storeEncryptedBackupByWallet({
5327
5756
  walletId,
5328
5757
  shareSetId: (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.shareSetId,
@@ -5330,6 +5759,7 @@ class DynamicWalletClient {
5330
5759
  passwordEncrypted: isPasswordEncrypted,
5331
5760
  encryptionVersion,
5332
5761
  signedSessionId,
5762
+ sessionPublicKey: resolvedSessionPublicKey,
5333
5763
  requiresSignedSessionId: this.requiresSignedSessionId(),
5334
5764
  dynamicRequestId
5335
5765
  });
@@ -5478,7 +5908,7 @@ class DynamicWalletClient {
5478
5908
  }
5479
5909
  return location;
5480
5910
  }
5481
- async backupSharesWithDistribution({ accountAddress, password, signedSessionId, distribution, preserveDelegatedLocation = false, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
5911
+ async backupSharesWithDistribution({ accountAddress, password, signedSessionId, sessionPublicKey, distribution, preserveDelegatedLocation = false, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
5482
5912
  const dynamicRequestId = v4();
5483
5913
  // Get wallet data early for logging context (also used in catch block)
5484
5914
  const walletData = this.getWalletFromMap(accountAddress);
@@ -5557,6 +5987,7 @@ class DynamicWalletClient {
5557
5987
  clientShares: distribution.clientShares,
5558
5988
  password,
5559
5989
  signedSessionId: resolvedSignedSessionId,
5990
+ sessionPublicKey,
5560
5991
  dynamicRequestId,
5561
5992
  chainName: walletData.chainName,
5562
5993
  bitcoinConfig,
@@ -5753,29 +6184,23 @@ class DynamicWalletClient {
5753
6184
  // re-enter the outer operation with correct context. Skipped on internal post-rotation
5754
6185
  // flows that pass clientKeyShares explicitly (the refresh path).
5755
6186
  async ensureLocalSharesAreFresh({ accountAddress, walletData, localShares, password, signedSessionId }) {
5756
- var _freshBackupInfo_backups;
5757
6187
  // Always round-trip to the server — walletMap is populated at wallet-load and is
5758
6188
  // not invalidated when another tab rotates shares, so trusting it would
5759
6189
  // false-positive on cross-tab refresh.
5760
6190
  const { chainName } = walletData;
5761
- const mpcSigner = getMPCSigner({
5762
- chainName,
5763
- baseRelayUrl: this.baseMPCRelayApiUrl,
5764
- bitcoinConfig: this.getBitcoinConfigForChain(chainName, accountAddress)
6191
+ const freshBackupInfo = await this.fetchFreshWalletBackupInfo({
6192
+ accountAddress
5765
6193
  });
5766
- const [freshBackupInfo, localKeygenIds] = await Promise.all([
5767
- this.fetchFreshWalletBackupInfo({
5768
- accountAddress
5769
- }),
5770
- Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)))
5771
- ]);
5772
- var _freshBackupInfo_backups_BackupLocation_DYNAMIC;
5773
- const recordedKeygenIds = ((_freshBackupInfo_backups_BackupLocation_DYNAMIC = freshBackupInfo == null ? void 0 : (_freshBackupInfo_backups = freshBackupInfo.backups) == null ? void 0 : _freshBackupInfo_backups[BackupLocation.DYNAMIC]) != null ? _freshBackupInfo_backups_BackupLocation_DYNAMIC : []).map((entry)=>entry.keygenId).filter((id)=>typeof id === 'string' && id.length > 0);
6194
+ const recordedKeygenIds = recordedDynamicKeygenIds(freshBackupInfo);
5774
6195
  // First-time backup or legacy data (server didn't populate keygenId yet) — nothing to compare.
5775
6196
  if (recordedKeygenIds.length === 0) return;
5776
- const recordedSet = new Set(recordedKeygenIds);
5777
- const localMatchesRecorded = localKeygenIds.length === recordedSet.size && localKeygenIds.every((id)=>recordedSet.has(id));
5778
- if (localMatchesRecorded) return;
6197
+ const { fresh, localKeygenIds } = await this.compareShareGenerations({
6198
+ accountAddress,
6199
+ chainName,
6200
+ localShares,
6201
+ recordedKeygenIds
6202
+ });
6203
+ if (fresh) return;
5779
6204
  this.logger.warn('[storeEncryptedBackupByWallet] Stale local shares detected; refreshing local storage', {
5780
6205
  accountAddress,
5781
6206
  walletId: walletData.walletId,
@@ -5802,7 +6227,7 @@ class DynamicWalletClient {
5802
6227
  const recoveryPassword = freshBackupInfo.passwordEncrypted ? password : undefined;
5803
6228
  try {
5804
6229
  // walletMap was already refreshed by fetchFreshWalletBackupInfo above, so we skip the
5805
- // getWallets() refresh that recoverKeySharesOnMismatch performs for its own callers.
6230
+ // getWallets() refresh that verifyAndRecoverStaleShare performs for its own callers.
5806
6231
  await this.internalRecoverEncryptedBackupByWallet({
5807
6232
  accountAddress,
5808
6233
  password: recoveryPassword,
@@ -5860,7 +6285,7 @@ class DynamicWalletClient {
5860
6285
  * @param params.signedSessionId - Optional signed session ID for authentication
5861
6286
  * @param params.backupToGoogleDrive - Whether to backup to Google Drive (defaults to false)
5862
6287
  * @returns Promise with backup metadata including share locations and IDs
5863
- */ async storeEncryptedBackupByWallet({ accountAddress, clientKeyShares = undefined, password = undefined, signedSessionId, cloudProviders = [], delegatedKeyshare = undefined, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
6288
+ */ async storeEncryptedBackupByWallet({ accountAddress, clientKeyShares = undefined, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegatedKeyshare = undefined, passwordUpdateBatchId, googleDriveAccessToken, googleDriveTokenSource, businessAccountId, encryptionVersion }) {
5864
6289
  var _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
5865
6290
  const hasProvidedShares = clientKeyShares !== undefined;
5866
6291
  const keySharesToBackup = clientKeyShares != null ? clientKeyShares : await this.getClientKeySharesFromStorage({
@@ -5930,6 +6355,7 @@ class DynamicWalletClient {
5930
6355
  accountAddress,
5931
6356
  password,
5932
6357
  signedSessionId,
6358
+ sessionPublicKey,
5933
6359
  distribution,
5934
6360
  preserveDelegatedLocation,
5935
6361
  passwordUpdateBatchId,
@@ -6002,7 +6428,7 @@ class DynamicWalletClient {
6002
6428
  errorContext
6003
6429
  } : {}));
6004
6430
  }
6005
- async updatePassword({ accountAddress, existingPassword, newPassword, signedSessionId, passwordUpdateBatchId }) {
6431
+ async updatePassword({ accountAddress, existingPassword, newPassword, signedSessionId, sessionPublicKey, passwordUpdateBatchId }) {
6006
6432
  const dynamicRequestId = v4();
6007
6433
  try {
6008
6434
  const wallet = this.getWalletFromMap(accountAddress);
@@ -6022,6 +6448,7 @@ class DynamicWalletClient {
6022
6448
  accountAddress,
6023
6449
  password: newPassword,
6024
6450
  signedSessionId,
6451
+ sessionPublicKey,
6025
6452
  passwordUpdateBatchId
6026
6453
  });
6027
6454
  this.logPasswordOperationSuccess('updatePassword', {
@@ -6051,7 +6478,7 @@ class DynamicWalletClient {
6051
6478
  throw error;
6052
6479
  }
6053
6480
  }
6054
- async setPassword({ accountAddress, newPassword, signedSessionId, passwordUpdateBatchId }) {
6481
+ async setPassword({ accountAddress, newPassword, signedSessionId, sessionPublicKey, passwordUpdateBatchId }) {
6055
6482
  const dynamicRequestId = v4();
6056
6483
  try {
6057
6484
  const wallet = this.getWalletFromMap(accountAddress);
@@ -6062,6 +6489,7 @@ class DynamicWalletClient {
6062
6489
  accountAddress,
6063
6490
  password: newPassword,
6064
6491
  signedSessionId,
6492
+ sessionPublicKey,
6065
6493
  passwordUpdateBatchId
6066
6494
  });
6067
6495
  this.logPasswordOperationSuccess('setPassword', {
@@ -6482,7 +6910,7 @@ class DynamicWalletClient {
6482
6910
  * Internal helper method that handles the complete flow for ensuring wallet key shares are backed up to a cloud provider.
6483
6911
  * - For 2-of-2 wallets: Automatically reshares to 2-of-3 threshold, then distributes shares (1 to backend, 1 to cloud)
6484
6912
  * - For 2-of-3 wallets: Call storeEncryptedBackupByWallet to backup for backend and cloud
6485
- */ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, backupLocation, googleDriveAccessToken, googleDriveTokenSource }) {
6913
+ */ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, sessionPublicKey, backupLocation, googleDriveAccessToken, googleDriveTokenSource }) {
6486
6914
  try {
6487
6915
  await this.getWallet({
6488
6916
  accountAddress,
@@ -6501,6 +6929,7 @@ class DynamicWalletClient {
6501
6929
  newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_THREE,
6502
6930
  password,
6503
6931
  signedSessionId,
6932
+ sessionPublicKey,
6504
6933
  cloudProviders: [
6505
6934
  backupLocation
6506
6935
  ],
@@ -6512,6 +6941,7 @@ class DynamicWalletClient {
6512
6941
  accountAddress,
6513
6942
  password,
6514
6943
  signedSessionId,
6944
+ sessionPublicKey,
6515
6945
  cloudProviders: [
6516
6946
  backupLocation
6517
6947
  ],
@@ -6542,7 +6972,7 @@ class DynamicWalletClient {
6542
6972
  * @param params.signedSessionId - Optional signed session ID for authentication
6543
6973
  * @param params.googleDriveAccessToken - Optional Google OAuth access token. When provided, the SDK uses it directly
6544
6974
  * instead of looking up the user's linked Google account and exchanging it for a token via the Dynamic backend.
6545
- */ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, googleDriveAccessToken }) {
6975
+ */ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, sessionPublicKey, googleDriveAccessToken }) {
6546
6976
  // Track whether the Drive token was supplied by the caller (external) or
6547
6977
  // will be resolved from the user's stored OAuth account (stored_oauth), so
6548
6978
  // backups using a token we don't control are auditable. Enum only — the
@@ -6562,6 +6992,7 @@ class DynamicWalletClient {
6562
6992
  accountAddress,
6563
6993
  password,
6564
6994
  signedSessionId,
6995
+ sessionPublicKey,
6565
6996
  backupLocation: BackupLocation.GOOGLE_DRIVE,
6566
6997
  // Forward the resolved token so the downstream upload path doesn't
6567
6998
  // re-fetch it via fetchGoogleDriveAccessToken.
@@ -6633,11 +7064,12 @@ class DynamicWalletClient {
6633
7064
  * @param params.accountAddress - The wallet account address to backup
6634
7065
  * @param params.password - Optional password for encryption (uses environment ID if not provided)
6635
7066
  * @param params.signedSessionId - Optional signed session ID for authentication
6636
- */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId }) {
7067
+ */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey }) {
6637
7068
  return this.backupKeySharesToCloudProvider({
6638
7069
  accountAddress,
6639
7070
  password,
6640
7071
  signedSessionId,
7072
+ sessionPublicKey,
6641
7073
  backupLocation: BackupLocation.ICLOUD
6642
7074
  });
6643
7075
  }
@@ -7850,6 +8282,10 @@ class DynamicWalletClient {
7850
8282
  if (internalOptions == null ? void 0 : internalOptions.getSignedSessionId) {
7851
8283
  this.getSignedSessionIdCallback = internalOptions.getSignedSessionId;
7852
8284
  }
8285
+ // Set session public key callback if provided (internal use only)
8286
+ if (internalOptions == null ? void 0 : internalOptions.getSessionPublicKey) {
8287
+ this.getSessionPublicKeyCallback = internalOptions.getSessionPublicKey;
8288
+ }
7853
8289
  // Read the callback lazily so it tracks `getSignedSessionIdCallback` even
7854
8290
  // when tests reassign it after construction.
7855
8291
  this.signedSession = new SignedSessionManager({
@@ -7923,4 +8359,4 @@ DynamicWalletClient.roomsPersistChain = Promise.resolve();
7923
8359
  // rooms back into the new user's storage (TOCTOU on the create→merge gap).
7924
8360
  DynamicWalletClient.roomsGeneration = 0;
7925
8361
 
7926
- export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_INCORRECT_PASSWORD, ERROR_KEYGEN_FAILED, ERROR_MULTIPLE_WALLETS_PER_CHAIN, ERROR_NO_KEY_SHARES_BACKED_UP, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, OperationSource, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, StaleLocalSharesError, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, formatTronMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getDelegatedShareSet, getGoogleOAuthAccountId, getHttpStatus, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isNonRetryableCeremonyError, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, listICloudBackups, markCeremonyErrorNonRetryable, readEnvironmentSettings, retryPromise, shouldReshareToSameBackups, timeoutPromise };
8362
+ export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_INCORRECT_PASSWORD, ERROR_KEYGEN_FAILED, ERROR_MULTIPLE_WALLETS_PER_CHAIN, ERROR_NO_KEY_SHARES_BACKED_UP, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, OperationSource, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, StaleLocalSharesError, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, formatTronMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getDelegatedShareSet, getGoogleOAuthAccountId, getHttpStatus, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isNonRetryableCeremonyError, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, isStaleClientSharesError, listICloudBackups, markCeremonyErrorNonRetryable, readEnvironmentSettings, retryPromise, shouldReshareToSameBackups, timeoutPromise };