@dynamic-labs-wallet/browser 1.1.56 → 1.1.57

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.cjs CHANGED
@@ -3857,7 +3857,9 @@ const validateOfflineRecoveryParams = ({ accountAddress, cloudProviderAccessToke
3857
3857
  ],
3858
3858
  offlineRecovery: true,
3859
3859
  mfaToken,
3860
- recoveryEncryptionPublicKey: params.recoveryEncryptionPublicKey
3860
+ recoveryEncryptionPublicKey: params.recoveryEncryptionPublicKey,
3861
+ approvalQuorum: params.approvalQuorum,
3862
+ proposalId: params.proposalId
3861
3863
  }, providerReshareArgs));
3862
3864
  core.Logger.info('offlineRecovery completed', {
3863
3865
  operation: 'offlineRecovery',
@@ -3987,6 +3989,25 @@ const BACKUP_OPERATION = {
3987
3989
  * path so normal signing stays quiet, well below the 20s queue timeout so a
3988
3990
  * stall is still captured while it is stalling.
3989
3991
  */ const SLOW_SIGN_PREFLIGHT_MS = 1000;
3992
+ // The server derives the gated operation from reshareKind, so a backup is
3993
+ // marked on every call, governed or not: unmarked, it reads as a plain reshare
3994
+ // and the backup's policy gate never runs.
3995
+ const reshareKindOf = ({ offlineRecovery, cloudBackup })=>{
3996
+ if (offlineRecovery) return 'offlineRecovery';
3997
+ return cloudBackup ? 'cloudBackup' : undefined;
3998
+ };
3999
+ /**
4000
+ * The reshare call resolves with an action-required body instead of a room
4001
+ * when a governed request is answered before the stream opens (a 422
4002
+ * `intent_required`). redcoast's reshare route answers on the stream today,
4003
+ * but a body with no room must never reach the ceremony: it would fail there
4004
+ * and roll back local shares that are intact.
4005
+ */ const assertReshareRoomOpened = (data)=>{
4006
+ if (core.isBusinessAccountActionRequired(data)) {
4007
+ throw new core.SigningConsentRequiredError(data);
4008
+ }
4009
+ return data;
4010
+ };
3990
4011
  /** A secure-storage key share read slower than this is logged on its own, so reads before the sign queue are visible too. */ const SLOW_KEY_SHARE_READ_MS = 1000;
3991
4012
  const KNOWN_SHARE_SET_TYPES = new Set([
3992
4013
  'rootUser',
@@ -6719,7 +6740,10 @@ class DynamicWalletClient {
6719
6740
  existingClientKeyShares
6720
6741
  };
6721
6742
  }
6722
- async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, initialSignerRules, recoveryEncryptionPublicKey }) {
6743
+ async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, initialSignerRules, recoveryEncryptionPublicKey, approvalQuorum, proposalId, cloudBackup = false }) {
6744
+ if (cloudBackup && (offlineRecovery || delegateToProjectEnvironment)) {
6745
+ throw new Error('reshare: cloudBackup excludes offlineRecovery and delegateToProjectEnvironment');
6746
+ }
6723
6747
  if (delegateToProjectEnvironment && offlineRecovery) {
6724
6748
  throw new Error('reshare: delegateToProjectEnvironment and offlineRecovery are mutually exclusive');
6725
6749
  }
@@ -6755,7 +6779,10 @@ class DynamicWalletClient {
6755
6779
  googleDriveAccessToken,
6756
6780
  googleDriveTokenSource,
6757
6781
  initialSignerRules,
6758
- recoveryEncryptionPublicKey
6782
+ recoveryEncryptionPublicKey,
6783
+ approvalQuorum,
6784
+ proposalId,
6785
+ cloudBackup
6759
6786
  }));
6760
6787
  }
6761
6788
  /** Reshare into a new 2/2 share set for the target signer; seed it to CKS
@@ -7094,6 +7121,7 @@ class DynamicWalletClient {
7094
7121
  completePromise,
7095
7122
  errorGate.promise
7096
7123
  ]),
7124
+ failed: errorGate.promise,
7097
7125
  get fired () {
7098
7126
  return fired;
7099
7127
  }
@@ -7278,7 +7306,9 @@ class DynamicWalletClient {
7278
7306
  * {@link recordReshareThresholdScheme} guard on the success path. Wiping
7279
7307
  * here would strand a wallet whose local keys are still valid.
7280
7308
  */ async rollbackFailedReshareLocalState({ accountAddress, oldThresholdSignatureScheme, offlineRecovery, error }) {
7281
- if (offlineRecovery || isPublicKeyMismatchError(error) || isStaleClientSharesError(error)) return;
7309
+ if (offlineRecovery || isPublicKeyMismatchError(error) || isStaleClientSharesError(error) || // Consent refusals happen before any ceremony starts — the local shares
7310
+ // are still good and must not be rolled back.
7311
+ error instanceof core.SigningConsentRequiredError) return;
7282
7312
  this.updateWalletMap(accountAddress, {
7283
7313
  thresholdSignatureScheme: oldThresholdSignatureScheme
7284
7314
  });
@@ -7315,7 +7345,7 @@ class DynamicWalletClient {
7315
7345
  clientKeySharesBackupInfo: updatedBackupInfo
7316
7346
  } : {}));
7317
7347
  }
7318
- async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false, initialSignerRules, recoveryEncryptionPublicKey }) {
7348
+ async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false, initialSignerRules, recoveryEncryptionPublicKey, approvalQuorum, proposalId, cloudBackup = false }) {
7319
7349
  const dynamicRequestId = uuid.v4();
7320
7350
  // Password validation - wrapped in try-catch for consistent error handling
7321
7351
  // This path should NOT wipe key shares on failure (shares remain valid)
@@ -7430,7 +7460,11 @@ class DynamicWalletClient {
7430
7460
  dynamicRequestId
7431
7461
  }
7432
7462
  });
7433
- const data = await this.apiClient.reshare(_extends({
7463
+ const reshareKind = reshareKindOf({
7464
+ offlineRecovery,
7465
+ cloudBackup
7466
+ });
7467
+ const data = assertReshareRoomOpened(await this.apiClient.reshare(_extends({
7434
7468
  walletId: wallet.walletId,
7435
7469
  shareSetId: wallet.shareSetId,
7436
7470
  clientKeygenIds: clientKeygenIds,
@@ -7442,17 +7476,19 @@ class DynamicWalletClient {
7442
7476
  elevatedAccessToken,
7443
7477
  revokeDelegation,
7444
7478
  initialSignerRules
7445
- }, offlineRecovery ? {
7446
- reshareKind: 'offlineRecovery'
7479
+ }, reshareKind ? {
7480
+ reshareKind
7447
7481
  } : {}, recoveryEncryptionPublicKey ? {
7448
7482
  offlineRecoveryEncryptionKey: {
7449
7483
  alg: 'HYBRID-RSA-AES-256',
7450
7484
  publicKeyPemB64: recoveryEncryptionPublicKey
7451
7485
  }
7452
7486
  } : {}, {
7487
+ approvalQuorum,
7488
+ proposalId,
7453
7489
  onError: ceremonyGate.onError,
7454
7490
  onCeremonyComplete: ceremonyGate.onCeremonyComplete
7455
- }));
7491
+ })));
7456
7492
  this.logger.info('[WaasReshare] room_created received, starting local MPC', {
7457
7493
  context: {
7458
7494
  walletId: wallet.walletId,
@@ -7497,49 +7533,23 @@ class DynamicWalletClient {
7497
7533
  let existingReshareResults;
7498
7534
  let newReshareResults;
7499
7535
  try {
7500
- const [existingSettled, newSettled] = await Promise.all([
7501
- Promise.allSettled(existingClientKeyShares.map((keyShare)=>mpcSigner.reshareRemainingParty(roomId, newMpcConfig.threshold, keyShare, allPartyKeygenIds))),
7502
- Promise.allSettled(newClientInitKeygenResults.map((keygenResult)=>mpcSigner.reshareNewParty(roomId, oldMpcConfig.threshold, newMpcConfig.threshold, keygenResult, allPartyKeygenIds)))
7503
- ]);
7504
- const collectRejections = (results, keygenIds, role)=>results.reduce((acc, r, i)=>{
7505
- if (r.status === 'rejected') {
7506
- acc.push({
7507
- role,
7508
- partyIndex: i,
7509
- keygenId: keygenIds[i],
7510
- error: r.reason
7511
- });
7512
- }
7513
- return acc;
7514
- }, []);
7515
- const failures = [
7516
- ...collectRejections(existingSettled, existingClientKeygenIds, 'existingParty'),
7517
- ...collectRejections(newSettled, newClientKeygenIds, 'newParty')
7518
- ];
7519
- if (failures.length > 0) {
7520
- const firstError = failures[0].error;
7521
- const error = firstError instanceof Error ? firstError : new Error(String(firstError));
7522
- logError({
7523
- message: 'Reshare MPC ceremony party failures',
7524
- error,
7525
- context: {
7526
- dynamicRequestId,
7527
- roomId,
7528
- failures: failures.map((f)=>({
7529
- role: f.role,
7530
- partyIndex: f.partyIndex,
7531
- keygenId: f.keygenId,
7532
- error: f.error instanceof Error ? f.error.message : String(f.error)
7533
- })),
7534
- existingSuccessCount: existingSettled.filter((r)=>r.status === 'fulfilled').length,
7535
- newSuccessCount: newSettled.filter((r)=>r.status === 'fulfilled').length
7536
- }
7537
- });
7538
- throw error;
7539
- }
7540
- existingReshareResults = existingSettled.map((r)=>r.value);
7541
- newReshareResults = newSettled.map((r)=>r.value);
7536
+ ({ existingReshareResults, newReshareResults } = await this.runReshareParties({
7537
+ mpcSigner,
7538
+ roomId,
7539
+ oldThreshold: oldMpcConfig.threshold,
7540
+ newThreshold: newMpcConfig.threshold,
7541
+ allPartyKeygenIds,
7542
+ existingClientKeyShares,
7543
+ existingClientKeygenIds,
7544
+ newClientInitKeygenResults,
7545
+ newClientKeygenIds,
7546
+ refused: ceremonyGate.failed,
7547
+ dynamicRequestId
7548
+ }));
7542
7549
  } catch (reshareError) {
7550
+ // A consent refusal never started the ceremony: the local shares are
7551
+ // intact, so there is nothing to heal.
7552
+ if (reshareError instanceof core.SigningConsentRequiredError) throw reshareError;
7543
7553
  // Heal stale local shares before rethrowing so a retry succeeds.
7544
7554
  await this.healStaleShares({
7545
7555
  accountAddress,
@@ -7669,7 +7679,10 @@ class DynamicWalletClient {
7669
7679
  googleDriveTokenSource,
7670
7680
  staleShareRetry: true,
7671
7681
  initialSignerRules,
7672
- recoveryEncryptionPublicKey
7682
+ recoveryEncryptionPublicKey,
7683
+ approvalQuorum,
7684
+ proposalId,
7685
+ cloudBackup
7673
7686
  });
7674
7687
  }
7675
7688
  logError({
@@ -7695,7 +7708,67 @@ class DynamicWalletClient {
7695
7708
  throw error;
7696
7709
  }
7697
7710
  }
7698
- async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName, initialSignerRules }) {
7711
+ /**
7712
+ * Runs the client's MPC parties for a reshare until they finish or the
7713
+ * server refuses the reshare on the stream, whichever comes first.
7714
+ */ async runReshareParties({ mpcSigner, roomId, oldThreshold, newThreshold, allPartyKeygenIds, existingClientKeyShares, existingClientKeygenIds, newClientInitKeygenResults, newClientKeygenIds, refused, dynamicRequestId }) {
7715
+ // redcoast answers room_created before wallet-service has accepted the
7716
+ // reshare, so a refusal (consent_required, policy violation) lands on
7717
+ // the stream while the local parties wait for a server that never
7718
+ // joins. Race them against it, as export does, so the refusal surfaces
7719
+ // now rather than as a relay timeout.
7720
+ const mpcPartiesPromise = Promise.all([
7721
+ Promise.allSettled(existingClientKeyShares.map((keyShare)=>mpcSigner.reshareRemainingParty(roomId, newThreshold, keyShare, allPartyKeygenIds))),
7722
+ Promise.allSettled(newClientInitKeygenResults.map((keygenResult)=>mpcSigner.reshareNewParty(roomId, oldThreshold, newThreshold, keygenResult, allPartyKeygenIds)))
7723
+ ]);
7724
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
7725
+ mpcPartiesPromise.catch(()=>{});
7726
+ const [existingSettled, newSettled] = await Promise.race([
7727
+ mpcPartiesPromise,
7728
+ refused
7729
+ ]);
7730
+ const collectRejections = (results, keygenIds, role)=>results.reduce((acc, r, i)=>{
7731
+ if (r.status === 'rejected') {
7732
+ acc.push({
7733
+ role,
7734
+ partyIndex: i,
7735
+ keygenId: keygenIds[i],
7736
+ error: r.reason
7737
+ });
7738
+ }
7739
+ return acc;
7740
+ }, []);
7741
+ const failures = [
7742
+ ...collectRejections(existingSettled, existingClientKeygenIds, 'existingParty'),
7743
+ ...collectRejections(newSettled, newClientKeygenIds, 'newParty')
7744
+ ];
7745
+ if (failures.length > 0) {
7746
+ const firstError = failures[0].error;
7747
+ const error = firstError instanceof Error ? firstError : new Error(String(firstError));
7748
+ logError({
7749
+ message: 'Reshare MPC ceremony party failures',
7750
+ error,
7751
+ context: {
7752
+ dynamicRequestId,
7753
+ roomId,
7754
+ failures: failures.map((f)=>({
7755
+ role: f.role,
7756
+ partyIndex: f.partyIndex,
7757
+ keygenId: f.keygenId,
7758
+ error: f.error instanceof Error ? f.error.message : String(f.error)
7759
+ })),
7760
+ existingSuccessCount: existingSettled.filter((r)=>r.status === 'fulfilled').length,
7761
+ newSuccessCount: newSettled.filter((r)=>r.status === 'fulfilled').length
7762
+ }
7763
+ });
7764
+ throw error;
7765
+ }
7766
+ return {
7767
+ existingReshareResults: existingSettled.map((r)=>r.value),
7768
+ newReshareResults: newSettled.map((r)=>r.value)
7769
+ };
7770
+ }
7771
+ async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName, initialSignerRules, approvalQuorum, proposalId }) {
7699
7772
  try {
7700
7773
  const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[core.FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
7701
7774
  if (!delegateToProjectEnvironment) {
@@ -7732,7 +7805,9 @@ class DynamicWalletClient {
7732
7805
  mfaToken,
7733
7806
  revokeDelegation,
7734
7807
  // Do not attach rules on the revoke path.
7735
- initialSignerRules: revokeDelegation ? undefined : initialSignerRules
7808
+ initialSignerRules: revokeDelegation ? undefined : initialSignerRules,
7809
+ approvalQuorum,
7810
+ proposalId
7736
7811
  });
7737
7812
  } catch (error) {
7738
7813
  logError({
@@ -7745,7 +7820,7 @@ class DynamicWalletClient {
7745
7820
  throw error;
7746
7821
  }
7747
7822
  }
7748
- async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, initialSignerRules }) {
7823
+ async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, initialSignerRules, approvalQuorum, proposalId }) {
7749
7824
  var _this_featureFlags;
7750
7825
  const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[core.FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
7751
7826
  if (useShareSetReshare) {
@@ -7780,15 +7855,12 @@ class DynamicWalletClient {
7780
7855
  signedSessionId,
7781
7856
  sessionPublicKey,
7782
7857
  mfaToken,
7783
- // FF on: reshare to 2/2 — the new delegated share set is always 2/2
7784
- // (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
7785
- // + delegation case that returns newClientShareCount=1 (the share
7786
- // routed to the webhook) so the ceremony generates it correctly.
7787
- // FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
7788
- newThresholdSignatureScheme: useShareSetReshare ? core.ThresholdSignatureScheme.TWO_OF_TWO : core.ThresholdSignatureScheme.TWO_OF_THREE,
7858
+ newThresholdSignatureScheme: this.delegationThresholdSignatureScheme(useShareSetReshare),
7789
7859
  operationName: 'delegateKeyShares',
7790
7860
  useShareSetReshare,
7791
- initialSignerRules
7861
+ initialSignerRules,
7862
+ approvalQuorum,
7863
+ proposalId
7792
7864
  });
7793
7865
  const backupInfo = this.getRequiredWalletFromMap(accountAddress).clientKeySharesBackupInfo;
7794
7866
  const delegatedKeyShares = backupInfo.backups[core.BackupLocation.DELEGATED] || [];
@@ -7852,7 +7924,7 @@ class DynamicWalletClient {
7852
7924
  * operation logic lives in `./offlineRecovery.ts` (cloud-provider-agnostic,
7853
7925
  * so iCloud / other providers slot in there); this method just drives it
7854
7926
  * with the client instance and returns the exported set's own backups.
7855
- */ async offlineRecovery({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, cloudProvider = DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER, cloudProviderAccessToken, recoveryEncryptionPublicKey }) {
7927
+ */ async offlineRecovery({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, cloudProvider = DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER, cloudProviderAccessToken, recoveryEncryptionPublicKey, approvalQuorum, proposalId }) {
7856
7928
  await performOfflineRecoveryOperation(this, {
7857
7929
  accountAddress,
7858
7930
  password,
@@ -7861,7 +7933,9 @@ class DynamicWalletClient {
7861
7933
  mfaToken,
7862
7934
  cloudProvider,
7863
7935
  cloudProviderAccessToken,
7864
- recoveryEncryptionPublicKey
7936
+ recoveryEncryptionPublicKey,
7937
+ approvalQuorum,
7938
+ proposalId
7865
7939
  });
7866
7940
  // The exported set's own locations. Not `backups[cloudProvider]`: that is
7867
7941
  // rootUser's record for the provider, which this operation never writes.
@@ -7916,6 +7990,73 @@ class DynamicWalletClient {
7916
7990
  initiatorUserId
7917
7991
  });
7918
7992
  }
7993
+ /** The scheme a delegation reshare moves the wallet to: share-set delegation mints its own 2/2 set. */ delegationThresholdSignatureScheme(useShareSetReshare) {
7994
+ // FF on: reshare to 2/2 — the new delegated share set is always 2/2
7995
+ // (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
7996
+ // + delegation case that returns newClientShareCount=1 (the share
7997
+ // routed to the webhook) so the ceremony generates it correctly.
7998
+ // FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
7999
+ return useShareSetReshare ? core.ThresholdSignatureScheme.TWO_OF_TWO : core.ThresholdSignatureScheme.TWO_OF_THREE;
8000
+ }
8001
+ /**
8002
+ * Builds the reshare Intent for a governed reshare entry point, to be
8003
+ * signed with the session key and sent as `approvalQuorum` on that same
8004
+ * entry point (`backupKeySharesToGoogleDrive`/`backupKeySharesToICloud`,
8005
+ * `delegateKeyShares` or `offlineRecovery`).
8006
+ *
8007
+ * The Intent echoes the fields that entry point sends, so it is built here
8008
+ * from the same wallet state. An offline recovery's Intent must also echo
8009
+ * the recovery key the server will use: pass the one the `INTENT_REQUIRED`
8010
+ * consent error carried. Makes no API call and starts no ceremony.
8011
+ */ async prepareReshareIntent({ accountAddress, password = undefined, signedSessionId, businessAccountId, initiatorUserId, completeBy, operation, offlineRecoveryEncryptionKey }) {
8012
+ if (operation === 'offlineRecovery' !== !!offlineRecoveryEncryptionKey) {
8013
+ throw new Error('prepareReshareIntent: offlineRecoveryEncryptionKey is required for offlineRecovery, from its INTENT_REQUIRED consent error, and invalid otherwise');
8014
+ }
8015
+ // Refreshed from the server, as the backup entry point does before it
8016
+ // picks its route: a wallet delegated from another device since it was
8017
+ // cached backs up without a reshare, and must not get an intent for one.
8018
+ await this.getWallet({
8019
+ accountAddress,
8020
+ password,
8021
+ signedSessionId,
8022
+ refresh: true
8023
+ });
8024
+ const walletData = this.getRequiredWalletFromMap(accountAddress);
8025
+ const identity = {
8026
+ accountAddress,
8027
+ businessAccountId,
8028
+ completeBy,
8029
+ initiatorUserId
8030
+ };
8031
+ if (operation === 'delegatedAccess') {
8032
+ var _this_featureFlags;
8033
+ const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[core.FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
8034
+ return core.createReshareIntent(_extends({}, identity, {
8035
+ newThresholdSignatureScheme: this.delegationThresholdSignatureScheme(useShareSetReshare),
8036
+ reshareType: 'delegatedAccess'
8037
+ }));
8038
+ }
8039
+ if (operation === 'offlineRecovery') {
8040
+ return core.createReshareIntent(_extends({}, identity, {
8041
+ // performOfflineRecoveryOperation always pins the exported set at 2/2.
8042
+ newThresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
8043
+ offlineRecoveryEncryptionKey,
8044
+ reshareType: 'offlineRecovery'
8045
+ }));
8046
+ }
8047
+ // A cloud backup reshares only from 2/2, or from a non-delegated 2/3
8048
+ // (backupKeySharesToCloudProvider); any other wallet backs up without a
8049
+ // reshare, so there is nothing to approve.
8050
+ const scheme = walletData.thresholdSignatureScheme;
8051
+ const reshares = scheme === core.ThresholdSignatureScheme.TWO_OF_TWO || scheme === core.ThresholdSignatureScheme.TWO_OF_THREE && !hasDelegatedBackup(walletData.clientKeySharesBackupInfo, walletData.otherShareSets);
8052
+ if (!reshares) {
8053
+ throw new Error('prepareReshareIntent: this wallet backs up without a reshare, so no approval applies');
8054
+ }
8055
+ return core.createReshareIntent(_extends({}, identity, {
8056
+ newThresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_THREE,
8057
+ reshareType: 'cloudBackup'
8058
+ }));
8059
+ }
7919
8060
  async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, proposalId, traceContext }) {
7920
8061
  // Public entry point delegates to the private implementation. Keeping the
7921
8062
  // `staleShareRetry` flag off the public signature mirrors sign/refresh/
@@ -10083,7 +10224,7 @@ class DynamicWalletClient {
10083
10224
  * - For 2-of-2 wallets: Automatically reshares to 2-of-3 threshold, then distributes shares (1 to backend, 1 to cloud)
10084
10225
  * - For non-delegated 2-of-3 wallets: same-scheme reshare to mint a fresh recovery share
10085
10226
  * - For delegated 2-of-3 (and other) wallets: storeEncryptedBackupByWallet backs up to backend and cloud
10086
- */ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, sessionPublicKey, backupLocation, googleDriveAccessToken, googleDriveTokenSource }) {
10227
+ */ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, sessionPublicKey, backupLocation, googleDriveAccessToken, googleDriveTokenSource, approvalQuorum, proposalId }) {
10087
10228
  try {
10088
10229
  await this.getWallet({
10089
10230
  accountAddress,
@@ -10107,7 +10248,10 @@ class DynamicWalletClient {
10107
10248
  backupLocation
10108
10249
  ],
10109
10250
  googleDriveAccessToken,
10110
- googleDriveTokenSource
10251
+ googleDriveTokenSource,
10252
+ cloudBackup: true,
10253
+ approvalQuorum,
10254
+ proposalId
10111
10255
  });
10112
10256
  } else if (currentThresholdSignatureScheme === core.ThresholdSignatureScheme.TWO_OF_THREE && !hasDelegatedBackup(walletData.clientKeySharesBackupInfo, walletData.otherShareSets)) {
10113
10257
  await this.reshareNonDelegatedTwoOfThree({
@@ -10117,7 +10261,9 @@ class DynamicWalletClient {
10117
10261
  signedSessionId,
10118
10262
  sessionPublicKey,
10119
10263
  googleDriveAccessToken,
10120
- googleDriveTokenSource
10264
+ googleDriveTokenSource,
10265
+ approvalQuorum,
10266
+ proposalId
10121
10267
  });
10122
10268
  } else {
10123
10269
  await this.storeEncryptedBackupByWallet({
@@ -10149,7 +10295,7 @@ class DynamicWalletClient {
10149
10295
  * reshare that mints a fresh recovery share. The fresh share is redistributed to
10150
10296
  * every provider already holding a backup plus the requested one,
10151
10297
  * restricted to providers the reshare can deliver to.
10152
- */ async reshareNonDelegatedTwoOfThree({ accountAddress, backupLocation, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, googleDriveTokenSource }) {
10298
+ */ async reshareNonDelegatedTwoOfThree({ accountAddress, backupLocation, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, googleDriveTokenSource, approvalQuorum, proposalId }) {
10153
10299
  const walletData = this.getRequiredWalletFromMap(accountAddress);
10154
10300
  const cloudProviders = new Set([
10155
10301
  ...getActiveCloudProviders(walletData.clientKeySharesBackupInfo).filter((provider)=>provider === core.BackupLocation.GOOGLE_DRIVE || provider === core.BackupLocation.ICLOUD),
@@ -10184,7 +10330,10 @@ class DynamicWalletClient {
10184
10330
  sessionPublicKey,
10185
10331
  cloudProviders: Array.from(cloudProviders),
10186
10332
  googleDriveAccessToken: resolvedDrive.googleDriveAccessToken,
10187
- googleDriveTokenSource: resolvedDrive.googleDriveTokenSource
10333
+ googleDriveTokenSource: resolvedDrive.googleDriveTokenSource,
10334
+ cloudBackup: true,
10335
+ approvalQuorum,
10336
+ proposalId
10188
10337
  });
10189
10338
  }
10190
10339
  /**
@@ -10254,7 +10403,7 @@ class DynamicWalletClient {
10254
10403
  * @param params.signedSessionId - Optional signed session ID for authentication
10255
10404
  * @param params.googleDriveAccessToken - Optional Google OAuth access token. When provided, the SDK uses it directly
10256
10405
  * instead of looking up the user's linked Google account and exchanging it for a token via the Dynamic backend.
10257
- */ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, sessionPublicKey, googleDriveAccessToken }) {
10406
+ */ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, approvalQuorum, proposalId }) {
10258
10407
  // Track whether the Drive token was supplied by the caller (external) or
10259
10408
  // will be resolved from the user's stored OAuth account (stored_oauth), so
10260
10409
  // backups using a token we don't control are auditable. Enum only — the
@@ -10292,7 +10441,9 @@ class DynamicWalletClient {
10292
10441
  googleDriveAccessToken: resolvedAccessToken,
10293
10442
  // Persist which token source backed up the Google Drive location for
10294
10443
  // audit (enum only — never the token value).
10295
- googleDriveTokenSource: tokenSource
10444
+ googleDriveTokenSource: tokenSource,
10445
+ approvalQuorum,
10446
+ proposalId
10296
10447
  });
10297
10448
  success = true;
10298
10449
  } finally{
@@ -10365,13 +10516,23 @@ class DynamicWalletClient {
10365
10516
  * @param params.accountAddress - The wallet account address to backup
10366
10517
  * @param params.password - Optional password for encryption (uses environment ID if not provided)
10367
10518
  * @param params.signedSessionId - Optional signed session ID for authentication
10368
- */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey }) {
10519
+ */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey, approvalQuorum, proposalId }) {
10520
+ // Refreshed from the server, as the Google Drive backup and
10521
+ // prepareReshareIntent do: the route (reshare or not) must be picked from
10522
+ // the same wallet state the reshare intent was built from.
10523
+ await this.getWallet({
10524
+ accountAddress,
10525
+ signedSessionId,
10526
+ refresh: true
10527
+ });
10369
10528
  return this.backupKeySharesToCloudProvider({
10370
10529
  accountAddress,
10371
10530
  password,
10372
10531
  signedSessionId,
10373
10532
  sessionPublicKey,
10374
- backupLocation: core.BackupLocation.ICLOUD
10533
+ backupLocation: core.BackupLocation.ICLOUD,
10534
+ approvalQuorum,
10535
+ proposalId
10375
10536
  });
10376
10537
  }
10377
10538
  /**