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