@dynamic-labs-wallet/browser 1.1.56 → 1.1.58

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';
@@ -662,6 +662,12 @@ function meetsMinVersion(sdkVersion, { minVersionByNamespace, fallbackMinVersion
662
662
  }
663
663
  return resolved;
664
664
  }
665
+ /**
666
+ * Replaces a session whose single-use nonce was already spent. Keeps `spent` when no
667
+ * reverse channel can mint one, so legacy hosts behave as before.
668
+ */ async renew(spent) {
669
+ return this.canRefresh && this.supportsReverseChannel() ? this.resolve() : spent;
670
+ }
665
671
  /** Whether the configured SDK version requires a signed session ID. */ isRequired() {
666
672
  return this.required;
667
673
  }
@@ -3858,7 +3864,9 @@ const validateOfflineRecoveryParams = ({ accountAddress, cloudProviderAccessToke
3858
3864
  ],
3859
3865
  offlineRecovery: true,
3860
3866
  mfaToken,
3861
- recoveryEncryptionPublicKey: params.recoveryEncryptionPublicKey
3867
+ recoveryEncryptionPublicKey: params.recoveryEncryptionPublicKey,
3868
+ approvalQuorum: params.approvalQuorum,
3869
+ proposalId: params.proposalId
3862
3870
  }, providerReshareArgs));
3863
3871
  Logger.info('offlineRecovery completed', {
3864
3872
  operation: 'offlineRecovery',
@@ -3988,6 +3996,25 @@ const BACKUP_OPERATION = {
3988
3996
  * path so normal signing stays quiet, well below the 20s queue timeout so a
3989
3997
  * stall is still captured while it is stalling.
3990
3998
  */ const SLOW_SIGN_PREFLIGHT_MS = 1000;
3999
+ // The server derives the gated operation from reshareKind, so a backup is
4000
+ // marked on every call, governed or not: unmarked, it reads as a plain reshare
4001
+ // and the backup's policy gate never runs.
4002
+ const reshareKindOf = ({ offlineRecovery, cloudBackup })=>{
4003
+ if (offlineRecovery) return 'offlineRecovery';
4004
+ return cloudBackup ? 'cloudBackup' : undefined;
4005
+ };
4006
+ /**
4007
+ * The reshare call resolves with an action-required body instead of a room
4008
+ * when a governed request is answered before the stream opens (a 422
4009
+ * `intent_required`). redcoast's reshare route answers on the stream today,
4010
+ * but a body with no room must never reach the ceremony: it would fail there
4011
+ * and roll back local shares that are intact.
4012
+ */ const assertReshareRoomOpened = (data)=>{
4013
+ if (isBusinessAccountActionRequired(data)) {
4014
+ throw new SigningConsentRequiredError(data);
4015
+ }
4016
+ return data;
4017
+ };
3991
4018
  /** 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
4019
  const KNOWN_SHARE_SET_TYPES = new Set([
3993
4020
  'rootUser',
@@ -6720,7 +6747,10 @@ class DynamicWalletClient {
6720
6747
  existingClientKeyShares
6721
6748
  };
6722
6749
  }
6723
- async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, initialSignerRules, recoveryEncryptionPublicKey }) {
6750
+ 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 }) {
6751
+ if (cloudBackup && (offlineRecovery || delegateToProjectEnvironment)) {
6752
+ throw new Error('reshare: cloudBackup excludes offlineRecovery and delegateToProjectEnvironment');
6753
+ }
6724
6754
  if (delegateToProjectEnvironment && offlineRecovery) {
6725
6755
  throw new Error('reshare: delegateToProjectEnvironment and offlineRecovery are mutually exclusive');
6726
6756
  }
@@ -6756,7 +6786,10 @@ class DynamicWalletClient {
6756
6786
  googleDriveAccessToken,
6757
6787
  googleDriveTokenSource,
6758
6788
  initialSignerRules,
6759
- recoveryEncryptionPublicKey
6789
+ recoveryEncryptionPublicKey,
6790
+ approvalQuorum,
6791
+ proposalId,
6792
+ cloudBackup
6760
6793
  }));
6761
6794
  }
6762
6795
  /** Reshare into a new 2/2 share set for the target signer; seed it to CKS
@@ -7095,6 +7128,7 @@ class DynamicWalletClient {
7095
7128
  completePromise,
7096
7129
  errorGate.promise
7097
7130
  ]),
7131
+ failed: errorGate.promise,
7098
7132
  get fired () {
7099
7133
  return fired;
7100
7134
  }
@@ -7279,7 +7313,9 @@ class DynamicWalletClient {
7279
7313
  * {@link recordReshareThresholdScheme} guard on the success path. Wiping
7280
7314
  * here would strand a wallet whose local keys are still valid.
7281
7315
  */ async rollbackFailedReshareLocalState({ accountAddress, oldThresholdSignatureScheme, offlineRecovery, error }) {
7282
- if (offlineRecovery || isPublicKeyMismatchError(error) || isStaleClientSharesError(error)) return;
7316
+ if (offlineRecovery || isPublicKeyMismatchError(error) || isStaleClientSharesError(error) || // Consent refusals happen before any ceremony starts — the local shares
7317
+ // are still good and must not be rolled back.
7318
+ error instanceof SigningConsentRequiredError) return;
7283
7319
  this.updateWalletMap(accountAddress, {
7284
7320
  thresholdSignatureScheme: oldThresholdSignatureScheme
7285
7321
  });
@@ -7316,7 +7352,7 @@ class DynamicWalletClient {
7316
7352
  clientKeySharesBackupInfo: updatedBackupInfo
7317
7353
  } : {}));
7318
7354
  }
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 }) {
7355
+ 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
7356
  const dynamicRequestId = v4();
7321
7357
  // Password validation - wrapped in try-catch for consistent error handling
7322
7358
  // This path should NOT wipe key shares on failure (shares remain valid)
@@ -7431,7 +7467,11 @@ class DynamicWalletClient {
7431
7467
  dynamicRequestId
7432
7468
  }
7433
7469
  });
7434
- const data = await this.apiClient.reshare(_extends({
7470
+ const reshareKind = reshareKindOf({
7471
+ offlineRecovery,
7472
+ cloudBackup
7473
+ });
7474
+ const data = assertReshareRoomOpened(await this.apiClient.reshare(_extends({
7435
7475
  walletId: wallet.walletId,
7436
7476
  shareSetId: wallet.shareSetId,
7437
7477
  clientKeygenIds: clientKeygenIds,
@@ -7443,17 +7483,19 @@ class DynamicWalletClient {
7443
7483
  elevatedAccessToken,
7444
7484
  revokeDelegation,
7445
7485
  initialSignerRules
7446
- }, offlineRecovery ? {
7447
- reshareKind: 'offlineRecovery'
7486
+ }, reshareKind ? {
7487
+ reshareKind
7448
7488
  } : {}, recoveryEncryptionPublicKey ? {
7449
7489
  offlineRecoveryEncryptionKey: {
7450
7490
  alg: 'HYBRID-RSA-AES-256',
7451
7491
  publicKeyPemB64: recoveryEncryptionPublicKey
7452
7492
  }
7453
7493
  } : {}, {
7494
+ approvalQuorum,
7495
+ proposalId,
7454
7496
  onError: ceremonyGate.onError,
7455
7497
  onCeremonyComplete: ceremonyGate.onCeremonyComplete
7456
- }));
7498
+ })));
7457
7499
  this.logger.info('[WaasReshare] room_created received, starting local MPC', {
7458
7500
  context: {
7459
7501
  walletId: wallet.walletId,
@@ -7498,49 +7540,23 @@ class DynamicWalletClient {
7498
7540
  let existingReshareResults;
7499
7541
  let newReshareResults;
7500
7542
  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);
7543
+ ({ existingReshareResults, newReshareResults } = await this.runReshareParties({
7544
+ mpcSigner,
7545
+ roomId,
7546
+ oldThreshold: oldMpcConfig.threshold,
7547
+ newThreshold: newMpcConfig.threshold,
7548
+ allPartyKeygenIds,
7549
+ existingClientKeyShares,
7550
+ existingClientKeygenIds,
7551
+ newClientInitKeygenResults,
7552
+ newClientKeygenIds,
7553
+ refused: ceremonyGate.failed,
7554
+ dynamicRequestId
7555
+ }));
7543
7556
  } catch (reshareError) {
7557
+ // A consent refusal never started the ceremony: the local shares are
7558
+ // intact, so there is nothing to heal.
7559
+ if (reshareError instanceof SigningConsentRequiredError) throw reshareError;
7544
7560
  // Heal stale local shares before rethrowing so a retry succeeds.
7545
7561
  await this.healStaleShares({
7546
7562
  accountAddress,
@@ -7670,7 +7686,10 @@ class DynamicWalletClient {
7670
7686
  googleDriveTokenSource,
7671
7687
  staleShareRetry: true,
7672
7688
  initialSignerRules,
7673
- recoveryEncryptionPublicKey
7689
+ recoveryEncryptionPublicKey,
7690
+ approvalQuorum,
7691
+ proposalId,
7692
+ cloudBackup
7674
7693
  });
7675
7694
  }
7676
7695
  logError({
@@ -7696,7 +7715,67 @@ class DynamicWalletClient {
7696
7715
  throw error;
7697
7716
  }
7698
7717
  }
7699
- async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName, initialSignerRules }) {
7718
+ /**
7719
+ * Runs the client's MPC parties for a reshare until they finish or the
7720
+ * server refuses the reshare on the stream, whichever comes first.
7721
+ */ async runReshareParties({ mpcSigner, roomId, oldThreshold, newThreshold, allPartyKeygenIds, existingClientKeyShares, existingClientKeygenIds, newClientInitKeygenResults, newClientKeygenIds, refused, dynamicRequestId }) {
7722
+ // redcoast answers room_created before wallet-service has accepted the
7723
+ // reshare, so a refusal (consent_required, policy violation) lands on
7724
+ // the stream while the local parties wait for a server that never
7725
+ // joins. Race them against it, as export does, so the refusal surfaces
7726
+ // now rather than as a relay timeout.
7727
+ const mpcPartiesPromise = Promise.all([
7728
+ Promise.allSettled(existingClientKeyShares.map((keyShare)=>mpcSigner.reshareRemainingParty(roomId, newThreshold, keyShare, allPartyKeygenIds))),
7729
+ Promise.allSettled(newClientInitKeygenResults.map((keygenResult)=>mpcSigner.reshareNewParty(roomId, oldThreshold, newThreshold, keygenResult, allPartyKeygenIds)))
7730
+ ]);
7731
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
7732
+ mpcPartiesPromise.catch(()=>{});
7733
+ const [existingSettled, newSettled] = await Promise.race([
7734
+ mpcPartiesPromise,
7735
+ refused
7736
+ ]);
7737
+ const collectRejections = (results, keygenIds, role)=>results.reduce((acc, r, i)=>{
7738
+ if (r.status === 'rejected') {
7739
+ acc.push({
7740
+ role,
7741
+ partyIndex: i,
7742
+ keygenId: keygenIds[i],
7743
+ error: r.reason
7744
+ });
7745
+ }
7746
+ return acc;
7747
+ }, []);
7748
+ const failures = [
7749
+ ...collectRejections(existingSettled, existingClientKeygenIds, 'existingParty'),
7750
+ ...collectRejections(newSettled, newClientKeygenIds, 'newParty')
7751
+ ];
7752
+ if (failures.length > 0) {
7753
+ const firstError = failures[0].error;
7754
+ const error = firstError instanceof Error ? firstError : new Error(String(firstError));
7755
+ logError({
7756
+ message: 'Reshare MPC ceremony party failures',
7757
+ error,
7758
+ context: {
7759
+ dynamicRequestId,
7760
+ roomId,
7761
+ failures: failures.map((f)=>({
7762
+ role: f.role,
7763
+ partyIndex: f.partyIndex,
7764
+ keygenId: f.keygenId,
7765
+ error: f.error instanceof Error ? f.error.message : String(f.error)
7766
+ })),
7767
+ existingSuccessCount: existingSettled.filter((r)=>r.status === 'fulfilled').length,
7768
+ newSuccessCount: newSettled.filter((r)=>r.status === 'fulfilled').length
7769
+ }
7770
+ });
7771
+ throw error;
7772
+ }
7773
+ return {
7774
+ existingReshareResults: existingSettled.map((r)=>r.value),
7775
+ newReshareResults: newSettled.map((r)=>r.value)
7776
+ };
7777
+ }
7778
+ async performDelegationOperation({ accountAddress, password, signedSessionId, sessionPublicKey, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName, initialSignerRules, approvalQuorum, proposalId }) {
7700
7779
  try {
7701
7780
  const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
7702
7781
  if (!delegateToProjectEnvironment) {
@@ -7733,7 +7812,9 @@ class DynamicWalletClient {
7733
7812
  mfaToken,
7734
7813
  revokeDelegation,
7735
7814
  // Do not attach rules on the revoke path.
7736
- initialSignerRules: revokeDelegation ? undefined : initialSignerRules
7815
+ initialSignerRules: revokeDelegation ? undefined : initialSignerRules,
7816
+ approvalQuorum,
7817
+ proposalId
7737
7818
  });
7738
7819
  } catch (error) {
7739
7820
  logError({
@@ -7746,7 +7827,7 @@ class DynamicWalletClient {
7746
7827
  throw error;
7747
7828
  }
7748
7829
  }
7749
- async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, initialSignerRules }) {
7830
+ async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, initialSignerRules, approvalQuorum, proposalId }) {
7750
7831
  var _this_featureFlags;
7751
7832
  const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
7752
7833
  if (useShareSetReshare) {
@@ -7781,15 +7862,12 @@ class DynamicWalletClient {
7781
7862
  signedSessionId,
7782
7863
  sessionPublicKey,
7783
7864
  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,
7865
+ newThresholdSignatureScheme: this.delegationThresholdSignatureScheme(useShareSetReshare),
7790
7866
  operationName: 'delegateKeyShares',
7791
7867
  useShareSetReshare,
7792
- initialSignerRules
7868
+ initialSignerRules,
7869
+ approvalQuorum,
7870
+ proposalId
7793
7871
  });
7794
7872
  const backupInfo = this.getRequiredWalletFromMap(accountAddress).clientKeySharesBackupInfo;
7795
7873
  const delegatedKeyShares = backupInfo.backups[BackupLocation.DELEGATED] || [];
@@ -7853,7 +7931,7 @@ class DynamicWalletClient {
7853
7931
  * operation logic lives in `./offlineRecovery.ts` (cloud-provider-agnostic,
7854
7932
  * so iCloud / other providers slot in there); this method just drives it
7855
7933
  * 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 }) {
7934
+ */ async offlineRecovery({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, cloudProvider = DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER, cloudProviderAccessToken, recoveryEncryptionPublicKey, approvalQuorum, proposalId }) {
7857
7935
  await performOfflineRecoveryOperation(this, {
7858
7936
  accountAddress,
7859
7937
  password,
@@ -7862,7 +7940,9 @@ class DynamicWalletClient {
7862
7940
  mfaToken,
7863
7941
  cloudProvider,
7864
7942
  cloudProviderAccessToken,
7865
- recoveryEncryptionPublicKey
7943
+ recoveryEncryptionPublicKey,
7944
+ approvalQuorum,
7945
+ proposalId
7866
7946
  });
7867
7947
  // The exported set's own locations. Not `backups[cloudProvider]`: that is
7868
7948
  // rootUser's record for the provider, which this operation never writes.
@@ -7917,6 +7997,73 @@ class DynamicWalletClient {
7917
7997
  initiatorUserId
7918
7998
  });
7919
7999
  }
8000
+ /** The scheme a delegation reshare moves the wallet to: share-set delegation mints its own 2/2 set. */ delegationThresholdSignatureScheme(useShareSetReshare) {
8001
+ // FF on: reshare to 2/2 — the new delegated share set is always 2/2
8002
+ // (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
8003
+ // + delegation case that returns newClientShareCount=1 (the share
8004
+ // routed to the webhook) so the ceremony generates it correctly.
8005
+ // FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
8006
+ return useShareSetReshare ? ThresholdSignatureScheme.TWO_OF_TWO : ThresholdSignatureScheme.TWO_OF_THREE;
8007
+ }
8008
+ /**
8009
+ * Builds the reshare Intent for a governed reshare entry point, to be
8010
+ * signed with the session key and sent as `approvalQuorum` on that same
8011
+ * entry point (`backupKeySharesToGoogleDrive`/`backupKeySharesToICloud`,
8012
+ * `delegateKeyShares` or `offlineRecovery`).
8013
+ *
8014
+ * The Intent echoes the fields that entry point sends, so it is built here
8015
+ * from the same wallet state. An offline recovery's Intent must also echo
8016
+ * the recovery key the server will use: pass the one the `INTENT_REQUIRED`
8017
+ * consent error carried. Makes no API call and starts no ceremony.
8018
+ */ async prepareReshareIntent({ accountAddress, password = undefined, signedSessionId, businessAccountId, initiatorUserId, completeBy, operation, offlineRecoveryEncryptionKey }) {
8019
+ if (operation === 'offlineRecovery' !== !!offlineRecoveryEncryptionKey) {
8020
+ throw new Error('prepareReshareIntent: offlineRecoveryEncryptionKey is required for offlineRecovery, from its INTENT_REQUIRED consent error, and invalid otherwise');
8021
+ }
8022
+ // Refreshed from the server, as the backup entry point does before it
8023
+ // picks its route: a wallet delegated from another device since it was
8024
+ // cached backs up without a reshare, and must not get an intent for one.
8025
+ await this.getWallet({
8026
+ accountAddress,
8027
+ password,
8028
+ signedSessionId,
8029
+ refresh: true
8030
+ });
8031
+ const walletData = this.getRequiredWalletFromMap(accountAddress);
8032
+ const identity = {
8033
+ accountAddress,
8034
+ businessAccountId,
8035
+ completeBy,
8036
+ initiatorUserId
8037
+ };
8038
+ if (operation === 'delegatedAccess') {
8039
+ var _this_featureFlags;
8040
+ const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
8041
+ return createReshareIntent(_extends({}, identity, {
8042
+ newThresholdSignatureScheme: this.delegationThresholdSignatureScheme(useShareSetReshare),
8043
+ reshareType: 'delegatedAccess'
8044
+ }));
8045
+ }
8046
+ if (operation === 'offlineRecovery') {
8047
+ return createReshareIntent(_extends({}, identity, {
8048
+ // performOfflineRecoveryOperation always pins the exported set at 2/2.
8049
+ newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_TWO,
8050
+ offlineRecoveryEncryptionKey,
8051
+ reshareType: 'offlineRecovery'
8052
+ }));
8053
+ }
8054
+ // A cloud backup reshares only from 2/2, or from a non-delegated 2/3
8055
+ // (backupKeySharesToCloudProvider); any other wallet backs up without a
8056
+ // reshare, so there is nothing to approve.
8057
+ const scheme = walletData.thresholdSignatureScheme;
8058
+ const reshares = scheme === ThresholdSignatureScheme.TWO_OF_TWO || scheme === ThresholdSignatureScheme.TWO_OF_THREE && !hasDelegatedBackup(walletData.clientKeySharesBackupInfo, walletData.otherShareSets);
8059
+ if (!reshares) {
8060
+ throw new Error('prepareReshareIntent: this wallet backs up without a reshare, so no approval applies');
8061
+ }
8062
+ return createReshareIntent(_extends({}, identity, {
8063
+ newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_THREE,
8064
+ reshareType: 'cloudBackup'
8065
+ }));
8066
+ }
7920
8067
  async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, approvalQuorum, proposalId, traceContext }) {
7921
8068
  // Public entry point delegates to the private implementation. Keeping the
7922
8069
  // `staleShareRetry` flag off the public signature mirrors sign/refresh/
@@ -9541,11 +9688,12 @@ class DynamicWalletClient {
9541
9688
  passwordUpdateBatchId,
9542
9689
  dynamicRequestId
9543
9690
  });
9691
+ let spentSignedSession = false;
9544
9692
  if (isWalletPasswordEncrypted(wallet)) {
9545
9693
  if (!resolvedExistingPassword) {
9546
9694
  throw new Error(ERROR_EXISTING_PASSWORD_REQUIRED);
9547
9695
  }
9548
- await this.validatePasswordAgainstEncryptedShares({
9696
+ spentSignedSession = await this.validatePasswordAgainstEncryptedShares({
9549
9697
  wallet: wallet,
9550
9698
  address: accountAddress,
9551
9699
  password: resolvedExistingPassword,
@@ -9553,21 +9701,24 @@ class DynamicWalletClient {
9553
9701
  errorMessage: ERROR_INCORRECT_PASSWORD
9554
9702
  });
9555
9703
  }
9556
- const backup = ()=>this.storeEncryptedBackupByWallet({
9704
+ // Every relay call burns its session's single-use nonce, so the backup needs a fresh one.
9705
+ const backupSessionId = spentSignedSession ? await this.signedSession.renew(signedSessionId) : signedSessionId;
9706
+ const backup = (sessionId)=>this.storeEncryptedBackupByWallet({
9557
9707
  accountAddress,
9558
9708
  password: newPassword,
9559
9709
  existingPassword: resolvedExistingPassword,
9560
- signedSessionId,
9710
+ signedSessionId: sessionId,
9561
9711
  sessionPublicKey,
9562
9712
  passwordUpdateBatchId,
9563
9713
  passwordSetByBA: false
9564
9714
  });
9565
9715
  let backupData;
9566
9716
  try {
9567
- backupData = await backup();
9717
+ backupData = await backup(backupSessionId);
9568
9718
  } catch (error) {
9569
9719
  if (!(error instanceof StaleLocalSharesError) || !error.localSharesRefreshed) throw error;
9570
- backupData = await backup();
9720
+ // The stale-share refresh spent the session the failed attempt carried.
9721
+ backupData = await backup(await this.signedSession.renew(backupSessionId));
9571
9722
  }
9572
9723
  // Not named passwordUpdateStatus: the logger redacts string values of password* keys.
9573
9724
  this.logPasswordOperationSuccess('updatePassword', {
@@ -9706,6 +9857,8 @@ class DynamicWalletClient {
9706
9857
  */ async validatePasswordAgainstEncryptedShares({ wallet, address, password, signedSessionId, errorMessage = ERROR_PASSWORD_MISMATCH }) {
9707
9858
  const storageKey = `${normalizeAddress(address)}${ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
9708
9859
  let encryptedData = await this.storage.getItem(storageKey);
9860
+ // Fetching the shares spends the session's single-use nonce; callers must renew it.
9861
+ const spentSignedSession = !encryptedData;
9709
9862
  if (!encryptedData) {
9710
9863
  const { shares } = this.recoverStrategy({
9711
9864
  clientKeyShareBackupInfo: wallet.clientKeySharesBackupInfo,
@@ -9744,7 +9897,7 @@ class DynamicWalletClient {
9744
9897
  }
9745
9898
  const credential = extractFirstEncryptedCredential(encryptedData);
9746
9899
  if (!credential) {
9747
- return;
9900
+ return spentSignedSession;
9748
9901
  }
9749
9902
  try {
9750
9903
  await this.decryptKeyShare({
@@ -9757,6 +9910,7 @@ class DynamicWalletClient {
9757
9910
  }
9758
9911
  throw error;
9759
9912
  }
9913
+ return spentSignedSession;
9760
9914
  }
9761
9915
  isPasswordRequired() {
9762
9916
  var _readEnvironmentSettings_sdk_waas, _readEnvironmentSettings_sdk, _readEnvironmentSettings;
@@ -10084,7 +10238,7 @@ class DynamicWalletClient {
10084
10238
  * - For 2-of-2 wallets: Automatically reshares to 2-of-3 threshold, then distributes shares (1 to backend, 1 to cloud)
10085
10239
  * - For non-delegated 2-of-3 wallets: same-scheme reshare to mint a fresh recovery share
10086
10240
  * - 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 }) {
10241
+ */ async backupKeySharesToCloudProvider({ accountAddress, password, signedSessionId, sessionPublicKey, backupLocation, googleDriveAccessToken, googleDriveTokenSource, approvalQuorum, proposalId }) {
10088
10242
  try {
10089
10243
  await this.getWallet({
10090
10244
  accountAddress,
@@ -10108,7 +10262,10 @@ class DynamicWalletClient {
10108
10262
  backupLocation
10109
10263
  ],
10110
10264
  googleDriveAccessToken,
10111
- googleDriveTokenSource
10265
+ googleDriveTokenSource,
10266
+ cloudBackup: true,
10267
+ approvalQuorum,
10268
+ proposalId
10112
10269
  });
10113
10270
  } else if (currentThresholdSignatureScheme === ThresholdSignatureScheme.TWO_OF_THREE && !hasDelegatedBackup(walletData.clientKeySharesBackupInfo, walletData.otherShareSets)) {
10114
10271
  await this.reshareNonDelegatedTwoOfThree({
@@ -10118,7 +10275,9 @@ class DynamicWalletClient {
10118
10275
  signedSessionId,
10119
10276
  sessionPublicKey,
10120
10277
  googleDriveAccessToken,
10121
- googleDriveTokenSource
10278
+ googleDriveTokenSource,
10279
+ approvalQuorum,
10280
+ proposalId
10122
10281
  });
10123
10282
  } else {
10124
10283
  await this.storeEncryptedBackupByWallet({
@@ -10150,7 +10309,7 @@ class DynamicWalletClient {
10150
10309
  * reshare that mints a fresh recovery share. The fresh share is redistributed to
10151
10310
  * every provider already holding a backup plus the requested one,
10152
10311
  * restricted to providers the reshare can deliver to.
10153
- */ async reshareNonDelegatedTwoOfThree({ accountAddress, backupLocation, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, googleDriveTokenSource }) {
10312
+ */ async reshareNonDelegatedTwoOfThree({ accountAddress, backupLocation, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, googleDriveTokenSource, approvalQuorum, proposalId }) {
10154
10313
  const walletData = this.getRequiredWalletFromMap(accountAddress);
10155
10314
  const cloudProviders = new Set([
10156
10315
  ...getActiveCloudProviders(walletData.clientKeySharesBackupInfo).filter((provider)=>provider === BackupLocation.GOOGLE_DRIVE || provider === BackupLocation.ICLOUD),
@@ -10185,7 +10344,10 @@ class DynamicWalletClient {
10185
10344
  sessionPublicKey,
10186
10345
  cloudProviders: Array.from(cloudProviders),
10187
10346
  googleDriveAccessToken: resolvedDrive.googleDriveAccessToken,
10188
- googleDriveTokenSource: resolvedDrive.googleDriveTokenSource
10347
+ googleDriveTokenSource: resolvedDrive.googleDriveTokenSource,
10348
+ cloudBackup: true,
10349
+ approvalQuorum,
10350
+ proposalId
10189
10351
  });
10190
10352
  }
10191
10353
  /**
@@ -10255,7 +10417,7 @@ class DynamicWalletClient {
10255
10417
  * @param params.signedSessionId - Optional signed session ID for authentication
10256
10418
  * @param params.googleDriveAccessToken - Optional Google OAuth access token. When provided, the SDK uses it directly
10257
10419
  * 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 }) {
10420
+ */ async backupKeySharesToGoogleDrive({ accountAddress, password, signedSessionId, sessionPublicKey, googleDriveAccessToken, approvalQuorum, proposalId }) {
10259
10421
  // Track whether the Drive token was supplied by the caller (external) or
10260
10422
  // will be resolved from the user's stored OAuth account (stored_oauth), so
10261
10423
  // backups using a token we don't control are auditable. Enum only — the
@@ -10293,7 +10455,9 @@ class DynamicWalletClient {
10293
10455
  googleDriveAccessToken: resolvedAccessToken,
10294
10456
  // Persist which token source backed up the Google Drive location for
10295
10457
  // audit (enum only — never the token value).
10296
- googleDriveTokenSource: tokenSource
10458
+ googleDriveTokenSource: tokenSource,
10459
+ approvalQuorum,
10460
+ proposalId
10297
10461
  });
10298
10462
  success = true;
10299
10463
  } finally{
@@ -10366,13 +10530,23 @@ class DynamicWalletClient {
10366
10530
  * @param params.accountAddress - The wallet account address to backup
10367
10531
  * @param params.password - Optional password for encryption (uses environment ID if not provided)
10368
10532
  * @param params.signedSessionId - Optional signed session ID for authentication
10369
- */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey }) {
10533
+ */ async backupKeySharesToICloud({ accountAddress, password, signedSessionId, sessionPublicKey, approvalQuorum, proposalId }) {
10534
+ // Refreshed from the server, as the Google Drive backup and
10535
+ // prepareReshareIntent do: the route (reshare or not) must be picked from
10536
+ // the same wallet state the reshare intent was built from.
10537
+ await this.getWallet({
10538
+ accountAddress,
10539
+ signedSessionId,
10540
+ refresh: true
10541
+ });
10370
10542
  return this.backupKeySharesToCloudProvider({
10371
10543
  accountAddress,
10372
10544
  password,
10373
10545
  signedSessionId,
10374
10546
  sessionPublicKey,
10375
- backupLocation: BackupLocation.ICLOUD
10547
+ backupLocation: BackupLocation.ICLOUD,
10548
+ approvalQuorum,
10549
+ proposalId
10376
10550
  });
10377
10551
  }
10378
10552
  /**
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.58",
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.58",
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.58",
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",