@dynamic-labs-wallet/browser 1.0.2 → 1.0.3

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
@@ -230,6 +230,25 @@ class InvalidPasswordError extends Error {
230
230
  this.name = 'InvalidPasswordError';
231
231
  }
232
232
  }
233
+ const KEY_SHARE_DECRYPTION_ERROR = 'Decryption failed.';
234
+ /**
235
+ * Thrown when a key share cannot be decrypted but no user-supplied password was
236
+ * involved. Distinct from InvalidPasswordError, which is reserved for cases
237
+ * where a user-entered password fails to decrypt a password-encrypted wallet.
238
+ *
239
+ * Internal-only signal: the user-facing message is deliberately generic so it
240
+ * does not leak implementation detail (e.g. environmentId fallback,
241
+ * cross-environment cipher, KDF/version mismatch). The `name` and any attached
242
+ * `cause` carry the real diagnostic for logs / monitoring.
243
+ */ class KeyShareDecryptionError extends Error {
244
+ constructor(options){
245
+ super(KEY_SHARE_DECRYPTION_ERROR);
246
+ this.name = 'KeyShareDecryptionError';
247
+ if ((options == null ? void 0 : options.cause) !== undefined) {
248
+ this.cause = options.cause;
249
+ }
250
+ }
251
+ }
233
252
  /**
234
253
  * Check if an error is an OperationError from SubtleCrypto.decrypt,
235
254
  * which indicates authentication failure (wrong password/derived key)
@@ -515,7 +534,15 @@ const writeCachedReason = (error, reason)=>{
515
534
  // Frozen/sealed errors — ignore, just lose the cache on this path.
516
535
  }
517
536
  };
537
+ const isKeyShareDecryptionError = (error)=>error instanceof KeyShareDecryptionError || error instanceof Error && error.name === 'KeyShareDecryptionError';
518
538
  const computeReason = (error)=>{
539
+ // KeyShareDecryptionError represents a system-side decryption failure where
540
+ // no user password was involved (e.g. envId-fallback failed on a
541
+ // non-password-encrypted wallet). Classify ahead of the InvalidPasswordError
542
+ // check so it doesn't get mislabeled as `wrong_password`.
543
+ if (isKeyShareDecryptionError(error)) {
544
+ return 'decryption_failure';
545
+ }
519
546
  if (isPasswordMismatchError(error)) {
520
547
  return 'wrong_password';
521
548
  }
@@ -1054,6 +1081,7 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
1054
1081
  if ((error == null ? void 0 : error.isRetryable) === false) return true;
1055
1082
  if (isPasswordMismatchError(error)) return true;
1056
1083
  if (error instanceof InvalidPasswordError) return true;
1084
+ if (error instanceof KeyShareDecryptionError) return true;
1057
1085
  const message = error instanceof Error ? error.message : '';
1058
1086
  if (!message) return false;
1059
1087
  return NON_RETRYABLE_CEREMONY_ERROR_MESSAGES.some((msg)=>message.includes(msg));
@@ -2098,6 +2126,50 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
2098
2126
  /** Track which wallets are currently executing inside a sign operation.
2099
2127
  * Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
2100
2128
 
2129
+ // Pure routing helper: maps a completed reshare ceremony's results into the
2130
+ // ShareDistribution that storage/publish should follow. Lives outside the
2131
+ // client class so it's straightforward to reason about and unit-test
2132
+ // without spinning up DynamicWalletClient.
2133
+ const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders, existingReshareResults, newReshareResults })=>{
2134
+ const allClientShares = [
2135
+ ...existingReshareResults,
2136
+ ...newReshareResults
2137
+ ];
2138
+ // Share-set delegation signal: same-parties reshare with zero new client
2139
+ // party. The refreshed rootUser-client output IS the delegated share —
2140
+ // it goes to the webhook out-of-band, NOT to Dynamic backup, and local
2141
+ // cache stays with gen1.
2142
+ if (resolvedDelegation && newReshareResults.length === 0 && existingReshareResults.length === 1) {
2143
+ return {
2144
+ clientShares: [],
2145
+ cloudProviderShares: {},
2146
+ delegatedShare: existingReshareResults[0]
2147
+ };
2148
+ }
2149
+ if (resolvedDelegation && resolvedCloudProviders.length > 0) {
2150
+ return createDelegationWithCloudProviderDistribution({
2151
+ providers: resolvedCloudProviders,
2152
+ existingShares: existingReshareResults,
2153
+ delegatedShare: newReshareResults[0]
2154
+ });
2155
+ }
2156
+ if (resolvedDelegation) {
2157
+ return createDelegationOnlyDistribution({
2158
+ existingShares: existingReshareResults,
2159
+ delegatedShare: newReshareResults[0]
2160
+ });
2161
+ }
2162
+ if (resolvedCloudProviders.length > 0) {
2163
+ return createCloudProviderDistribution({
2164
+ providers: resolvedCloudProviders,
2165
+ allShares: allClientShares
2166
+ });
2167
+ }
2168
+ return createDynamicOnlyDistribution({
2169
+ allShares: allClientShares
2170
+ });
2171
+ };
2172
+
2101
2173
  const ALG_LABEL_RSA = 'HYBRID-RSA-AES-256';
2102
2174
  /**
2103
2175
  * Convert base64 to base64url encoding
@@ -3583,7 +3655,7 @@ class DynamicWalletClient {
3583
3655
  * existingClientKeyShares: ClientKeyShare[]
3584
3656
  * }>} Object containing new and existing client keygen results, IDs and shares
3585
3657
  * @todo Support higher to lower reshare strategies
3586
- */ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme }) {
3658
+ */ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, delegateToProjectEnvironment }) {
3587
3659
  const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
3588
3660
  const mpcSigner = getMPCSigner({
3589
3661
  chainName,
@@ -3593,7 +3665,8 @@ class DynamicWalletClient {
3593
3665
  // Determine share counts based on threshold signature schemes
3594
3666
  const { newClientShareCount, existingClientShareCount } = getReshareConfig({
3595
3667
  oldThresholdSignatureScheme,
3596
- newThresholdSignatureScheme
3668
+ newThresholdSignatureScheme,
3669
+ delegateToProjectEnvironment
3597
3670
  });
3598
3671
  // Create new client shares
3599
3672
  const newClientInitKeygenResults = await Promise.all(Array.from({
@@ -3699,16 +3772,20 @@ class DynamicWalletClient {
3699
3772
  throw backupError;
3700
3773
  }
3701
3774
  }
3702
- // Rotates walletMap.shareSetId when a ceremony_complete event reports a
3703
- // new rootUser share set. Skipped for `delegated` (rootUser unchanged;
3704
- // the delegation flow owns the otherShareSets[] write), `server`, and
3705
- // empty payloads (legacy redcoast that predates the share-set rollout —
3706
- // server falls back to walletId resolution on the follow-up backup call).
3775
+ // Persists the ceremony_complete payload into the wallet map.
3776
+ // - rootUser rotate walletMap.shareSetId (if it actually changed).
3777
+ // - delegated append/replace under otherShareSets[] via recordDelegatedShareSet
3778
+ // (requires newThresholdSignatureScheme passed in by the reshare caller).
3779
+ // Reshare delegation flow owns this branch; refresh hits the non-rootUser
3780
+ // skip path below because newThresholdSignatureScheme isn't threaded.
3781
+ // - server / undefined → no-op with a skip log so DataDog shows the path
3782
+ // (undefined is legacy redcoast that predates the share-set rollout —
3783
+ // server falls back to walletId resolution on the follow-up backup call).
3707
3784
  //
3708
3785
  // Each skip reason is logged distinctly so operators can grep DataDog for
3709
3786
  // "legacy server callbacks" vs "delegation skips" vs "no-op same id"
3710
3787
  // separately — three operationally different states.
3711
- rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, extraContext }) {
3788
+ rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, newThresholdSignatureScheme, extraContext }) {
3712
3789
  const baseContext = _extends({
3713
3790
  walletId: wallet.walletId,
3714
3791
  ceremony
@@ -3725,10 +3802,20 @@ class DynamicWalletClient {
3725
3802
  });
3726
3803
  return;
3727
3804
  }
3805
+ if (shareSetType === 'delegated' && newThresholdSignatureScheme) {
3806
+ this.recordDelegatedShareSet({
3807
+ accountAddress,
3808
+ wallet,
3809
+ newShareSetId: shareSetId,
3810
+ newThresholdSignatureScheme
3811
+ });
3812
+ return;
3813
+ }
3728
3814
  if (shareSetType !== 'rootUser') {
3729
- // 'delegated' delegation flow owns the otherShareSets[] write (#947).
3730
- // 'server' → internal server share set, not for SDK rotation.
3731
- // undefined → server emitted partial payload (already warned upstream).
3815
+ // 'delegated' (no newThresholdSignatureScheme, e.g. refresh) SDK doesn't
3816
+ // own the otherShareSets[] write from refresh.
3817
+ // 'server' internal server share set, not for SDK rotation.
3818
+ // undefined → server emitted partial payload (already warned upstream).
3732
3819
  this.logger.info('[WaasShareSet] rotation skipped: non-rootUser share-set type', {
3733
3820
  context: _extends({}, baseContext, {
3734
3821
  shareSetType,
@@ -3755,6 +3842,38 @@ class DynamicWalletClient {
3755
3842
  shareSetId
3756
3843
  });
3757
3844
  }
3845
+ // Records a new `delegated` share set under wallet.otherShareSets[],
3846
+ // replacing an existing delegated entry if one is already there (rather
3847
+ // than appending duplicates on retry). Only reachable from the reshare
3848
+ // delegation flow — refresh doesn't thread newThresholdSignatureScheme
3849
+ // so it falls through to the skip path before reaching here.
3850
+ recordDelegatedShareSet({ accountAddress, wallet, newShareSetId, newThresholdSignatureScheme }) {
3851
+ const walletProps = this.getWalletFromMap(accountAddress);
3852
+ var _walletProps_otherShareSets;
3853
+ const existing = (_walletProps_otherShareSets = walletProps == null ? void 0 : walletProps.otherShareSets) != null ? _walletProps_otherShareSets : [];
3854
+ const newEntry = {
3855
+ shareSetId: newShareSetId,
3856
+ shareSetType: 'delegated',
3857
+ thresholdSignatureScheme: newThresholdSignatureScheme,
3858
+ createdAt: new Date().toISOString()
3859
+ };
3860
+ const delegatedIdx = existing.findIndex((s)=>s.shareSetType === 'delegated');
3861
+ const updated = delegatedIdx >= 0 ? existing.map((s, i)=>i === delegatedIdx ? newEntry : s) : [
3862
+ ...existing,
3863
+ newEntry
3864
+ ];
3865
+ this.logger.info('[WaasShareSet] delegated shareSetId added by reshare ceremony', {
3866
+ context: {
3867
+ walletId: wallet.walletId,
3868
+ rootUserShareSetId: wallet.shareSetId,
3869
+ delegatedShareSetId: newShareSetId,
3870
+ replacedExistingDelegated: delegatedIdx >= 0
3871
+ }
3872
+ });
3873
+ this.updateWalletMap(accountAddress, {
3874
+ otherShareSets: updated
3875
+ });
3876
+ }
3758
3877
  async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false, googleDriveAccessToken }) {
3759
3878
  const dynamicRequestId = v4();
3760
3879
  // Password validation - wrapped in try-catch for consistent error handling
@@ -3797,7 +3916,8 @@ class DynamicWalletClient {
3797
3916
  try {
3798
3917
  const { existingClientShareCount } = getReshareConfig({
3799
3918
  oldThresholdSignatureScheme,
3800
- newThresholdSignatureScheme
3919
+ newThresholdSignatureScheme,
3920
+ delegateToProjectEnvironment: resolvedDelegation
3801
3921
  });
3802
3922
  const wallet = await this.getWallet({
3803
3923
  accountAddress,
@@ -3811,11 +3931,14 @@ class DynamicWalletClient {
3811
3931
  accountAddress,
3812
3932
  wallet,
3813
3933
  oldThresholdSignatureScheme,
3814
- newThresholdSignatureScheme
3934
+ newThresholdSignatureScheme,
3935
+ delegateToProjectEnvironment: resolvedDelegation
3815
3936
  });
3816
- // Ensure existing client key shares exist before reshare
3817
- if (existingClientKeyShares.length === 0) {
3818
- throw new Error(`Client key shares are required for reshare operation but none were found for account address: ${accountAddress}`);
3937
+ // Ensure existing client key shares match what getReshareConfig expected.
3938
+ // For most flows this is >= 1; for 2/2 → 2/2 + delegation it's 0 (the
3939
+ // ceremony has no surviving client-side party from the old quorum).
3940
+ if (existingClientKeyShares.length < existingClientShareCount) {
3941
+ throw new Error(`Expected ${existingClientShareCount} existing client key share(s) for reshare operation but only found ${existingClientKeyShares.length} for account address: ${accountAddress}`);
3819
3942
  }
3820
3943
  const clientKeygenIds = [
3821
3944
  ...newClientKeygenIds,
@@ -3879,6 +4002,7 @@ class DynamicWalletClient {
3879
4002
  ceremony: 'reshare',
3880
4003
  shareSetId,
3881
4004
  shareSetType,
4005
+ newThresholdSignatureScheme,
3882
4006
  extraContext: {
3883
4007
  oldThresholdSignatureScheme,
3884
4008
  newThresholdSignatureScheme,
@@ -4002,41 +4126,12 @@ class DynamicWalletClient {
4002
4126
  }
4003
4127
  throw reshareError;
4004
4128
  }
4005
- const allClientShares = [
4006
- ...existingReshareResults,
4007
- ...newReshareResults
4008
- ];
4009
- let distribution;
4010
- // Generic distribution logic - works with any cloud providers
4011
- // Use effective* variables which may have been populated from existing wallet state
4012
- if (resolvedDelegation && resolvedCloudProviders.length > 0) {
4013
- // Delegation + Cloud Providers: Client's existing share backs up to both Dynamic and cloud providers.
4014
- // The new share goes to the webhook for delegation.
4015
- distribution = createDelegationWithCloudProviderDistribution({
4016
- providers: resolvedCloudProviders,
4017
- existingShares: existingReshareResults,
4018
- delegatedShare: newReshareResults[0]
4019
- });
4020
- } else if (resolvedDelegation) {
4021
- // Delegation only: Client's existing share backs up to Dynamic.
4022
- // The new share goes to the webhook for delegation. No cloud provider backup.
4023
- distribution = createDelegationOnlyDistribution({
4024
- existingShares: existingReshareResults,
4025
- delegatedShare: newReshareResults[0]
4026
- });
4027
- } else if (resolvedCloudProviders.length > 0) {
4028
- // Cloud Providers only: Split shares between Dynamic (N-1) and cloud providers (1).
4029
- // The last share (new share) goes to cloud providers.
4030
- distribution = createCloudProviderDistribution({
4031
- providers: resolvedCloudProviders,
4032
- allShares: allClientShares
4033
- });
4034
- } else {
4035
- // No delegation, no cloud providers: All shares go to Dynamic backend only.
4036
- distribution = createDynamicOnlyDistribution({
4037
- allShares: allClientShares
4038
- });
4039
- }
4129
+ const distribution = selectReshareDistribution({
4130
+ resolvedDelegation,
4131
+ resolvedCloudProviders,
4132
+ existingReshareResults,
4133
+ newReshareResults
4134
+ });
4040
4135
  this.updateWalletMap(accountAddress, {
4041
4136
  thresholdSignatureScheme: newThresholdSignatureScheme
4042
4137
  });
@@ -4095,10 +4190,22 @@ class DynamicWalletClient {
4095
4190
  });
4096
4191
  // Store client key shares to storage (localStorage or secureStorage)
4097
4192
  // only after backup succeeds.
4098
- await this.setClientKeySharesToStorage({
4099
- accountAddress,
4100
- clientKeyShares: distribution.clientShares
4101
- });
4193
+ //
4194
+ // Share-set delegation (FF=on) skips this entirely — the ceremony
4195
+ // output's refreshed rootUser-client share went to the webhook
4196
+ // (delegatedShare), and the original gen1 rootUser-client share must
4197
+ // stay in local cache so the user can still sign rootUser. See the
4198
+ // mixed-share-sign isolation property tested in
4199
+ // `wallet-service/scripts/twoOfTwoReshareExperiment.ts`. Calling this
4200
+ // with `clientShares: []` would clear the local cache (per the
4201
+ // method's "full replacement" semantics), breaking signing until
4202
+ // recovery refetches from Dynamic.
4203
+ if (distribution.clientShares.length > 0) {
4204
+ await this.setClientKeySharesToStorage({
4205
+ accountAddress,
4206
+ clientKeyShares: distribution.clientShares
4207
+ });
4208
+ }
4102
4209
  // Operation summary — correlates against downstream sign failures /
4103
4210
  // pending-rotation reports. `ceremonyCallbackFired: false` here means
4104
4211
  // either the server is legacy (didn't emit ceremony_complete) or the
@@ -4140,7 +4247,7 @@ class DynamicWalletClient {
4140
4247
  throw error;
4141
4248
  }
4142
4249
  }
4143
- async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, operationName }) {
4250
+ async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
4144
4251
  try {
4145
4252
  const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
4146
4253
  if (!delegateToProjectEnvironment) {
@@ -4157,8 +4264,11 @@ class DynamicWalletClient {
4157
4264
  }
4158
4265
  const walletData = this.getWalletFromMap(accountAddress);
4159
4266
  const currentThresholdSignatureScheme = walletData.thresholdSignatureScheme;
4160
- // Get active cloud providers to maintain existing backups
4161
- const activeProviders = getActiveCloudProviders(walletData == null ? void 0 : walletData.clientKeySharesBackupInfo);
4267
+ // Under the share-set reshare flow, rootUser is not mutated — the
4268
+ // delegation creates a separate `delegated` share set. So we don't
4269
+ // re-pin existing cloud providers; preservation is irrelevant when the
4270
+ // primary share set is untouched.
4271
+ const activeProviders = useShareSetReshare ? [] : getActiveCloudProviders(walletData == null ? void 0 : walletData.clientKeySharesBackupInfo);
4162
4272
  await this.reshare({
4163
4273
  chainName: walletData.chainName,
4164
4274
  accountAddress,
@@ -4183,19 +4293,82 @@ class DynamicWalletClient {
4183
4293
  }
4184
4294
  }
4185
4295
  async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
4296
+ var _this_featureFlags;
4297
+ const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
4298
+ if (useShareSetReshare) {
4299
+ var _this_getWalletFromMap;
4300
+ // FF on: delegation creates a separate `delegated` share set without
4301
+ // touching rootUser. If the wallet already has one, this is a no-op.
4302
+ const existingDelegated = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
4303
+ if (existingDelegated) {
4304
+ var _this_getWalletFromMap1;
4305
+ this.logger.info('[WaasShareSet] delegateKeyShares no-op — wallet already has a delegated share set', {
4306
+ accountAddress,
4307
+ delegatedShareSetId: existingDelegated.shareSetId
4308
+ });
4309
+ const backupInfo = (_this_getWalletFromMap1 = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap1.clientKeySharesBackupInfo;
4310
+ return (backupInfo == null ? void 0 : backupInfo.backups[BackupLocation.DELEGATED]) || [];
4311
+ }
4312
+ this.logger.info('[WaasShareSet] delegateKeyShares using share-set reshare path', {
4313
+ accountAddress
4314
+ });
4315
+ }
4186
4316
  await this.performDelegationOperation({
4187
4317
  accountAddress,
4188
4318
  password,
4189
4319
  signedSessionId,
4190
4320
  mfaToken,
4191
- newThresholdSignatureScheme: ThresholdSignatureScheme.TWO_OF_THREE,
4192
- operationName: 'delegateKeyShares'
4321
+ // FF on: reshare to 2/2 — the new delegated share set is always 2/2
4322
+ // (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
4323
+ // + delegation case that returns newClientShareCount=1 (the share
4324
+ // routed to the webhook) so the ceremony generates it correctly.
4325
+ // FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
4326
+ newThresholdSignatureScheme: useShareSetReshare ? ThresholdSignatureScheme.TWO_OF_TWO : ThresholdSignatureScheme.TWO_OF_THREE,
4327
+ operationName: 'delegateKeyShares',
4328
+ useShareSetReshare
4193
4329
  });
4194
4330
  const backupInfo = this.getWalletFromMap(accountAddress).clientKeySharesBackupInfo;
4195
4331
  const delegatedKeyShares = backupInfo.backups[BackupLocation.DELEGATED] || [];
4196
4332
  return delegatedKeyShares;
4197
4333
  }
4198
4334
  async revokeDelegation({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
4335
+ var _this_featureFlags;
4336
+ const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
4337
+ if (useShareSetReshare) {
4338
+ var _this_getWalletFromMap;
4339
+ const wallet = await this.getWallet({
4340
+ accountAddress,
4341
+ walletOperation: WalletOperation.NO_OPERATION,
4342
+ password,
4343
+ signedSessionId
4344
+ });
4345
+ const delegatedShareSet = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
4346
+ if (!delegatedShareSet) {
4347
+ this.logger.info('[WaasShareSet] revokeDelegation called but no delegated share set found', {
4348
+ accountAddress
4349
+ });
4350
+ return;
4351
+ }
4352
+ this.logger.info('[WaasShareSet] revokeDelegation via share-set REST DELETE (no MPC ceremony)', {
4353
+ accountAddress,
4354
+ delegatedShareSetId: delegatedShareSet.shareSetId,
4355
+ walletId: wallet.walletId
4356
+ });
4357
+ await this.apiClient.revokeDelegation({
4358
+ walletId: wallet.walletId,
4359
+ shareSetId: delegatedShareSet.shareSetId,
4360
+ signedSessionId,
4361
+ dynamicRequestId: v4().replaceAll('-', '')
4362
+ });
4363
+ // Refresh wallet state so otherShareSets / backups reflect the cleared delegation
4364
+ await this.getWallet({
4365
+ accountAddress,
4366
+ walletOperation: WalletOperation.NO_OPERATION,
4367
+ password,
4368
+ signedSessionId
4369
+ });
4370
+ return;
4371
+ }
4199
4372
  await this.performDelegationOperation({
4200
4373
  accountAddress,
4201
4374
  password,
@@ -4742,7 +4915,7 @@ class DynamicWalletClient {
4742
4915
  hasDelegatedShare: !!distribution.delegatedShare
4743
4916
  }));
4744
4917
  try {
4745
- var _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
4918
+ var _this_getWalletFromMap, _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
4746
4919
  // `let` so the retry path can swap in a freshly-signed session via the reverse channel on 400.
4747
4920
  let resolvedSignedSessionId = await this.resolveSignedSessionId(signedSessionId);
4748
4921
  const canRefreshSignedSessionId = this.getSignedSessionIdCallback !== undefined;
@@ -4837,10 +5010,24 @@ class DynamicWalletClient {
4837
5010
  googleDriveAccessToken
4838
5011
  }));
4839
5012
  }
5013
+ // When this ceremony minted a separate `delegated` share set (FF=on
5014
+ // path), backup activation must target that share set — not rootUser —
5015
+ // so `/delegatedAccess/delivery` and `/backup/locations` resolve to the
5016
+ // pending delegated row instead of mutating rootUser. The FF=off path
5017
+ // leaves `otherShareSets` empty and falls back to rootUser, matching
5018
+ // legacy in-place delegation behavior.
5019
+ //
5020
+ // Read otherShareSets fresh from the wallet map — `recordDelegatedShareSet`
5021
+ // wrote the new entry during `ceremony_complete`, which fired between
5022
+ // when `walletData` was captured at the top of this method and now.
5023
+ const freshOtherShareSets = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets;
5024
+ const delegatedShareSet = distribution.delegatedShare ? getDelegatedShareSet(freshOtherShareSets) : undefined;
5025
+ var _delegatedShareSet_shareSetId;
5026
+ const targetShareSetId = (_delegatedShareSet_shareSetId = delegatedShareSet == null ? void 0 : delegatedShareSet.shareSetId) != null ? _delegatedShareSet_shareSetId : walletData.shareSetId;
4840
5027
  if (distribution.delegatedShare) {
4841
5028
  uploadPromises.push(retryPromise(()=>this.publishDelegatedShare({
4842
5029
  walletId: walletData.walletId,
4843
- shareSetId: walletData.shareSetId,
5030
+ shareSetId: targetShareSetId,
4844
5031
  delegatedShare: distribution.delegatedShare,
4845
5032
  signedSessionId: resolvedSignedSessionId,
4846
5033
  dynamicRequestId,
@@ -4870,7 +5057,10 @@ class DynamicWalletClient {
4870
5057
  // won't fix.
4871
5058
  const backupData = await retryPromise(()=>this.apiClient.markKeySharesAsBackedUp({
4872
5059
  walletId: walletData.walletId,
4873
- shareSetId: walletData.shareSetId,
5060
+ // Same routing rule as publishDelegatedShare above: when this
5061
+ // ceremony minted a separate `delegated` share set, the atomic
5062
+ // swap in /backup/locations must activate THAT row, not rootUser.
5063
+ shareSetId: targetShareSetId,
4874
5064
  locations,
4875
5065
  dynamicRequestId,
4876
5066
  passwordUpdateBatchId
@@ -5268,13 +5458,35 @@ class DynamicWalletClient {
5268
5458
  }
5269
5459
  async decryptKeyShare({ keyShare, password }) {
5270
5460
  const decodedKeyShare = JSON.parse(Buffer.from(keyShare, 'base64').toString());
5271
- const decryptedKeyShare = await decryptData({
5272
- data: decodedKeyShare,
5273
- password: password != null ? password : this.environmentId
5274
- });
5275
- this.logPasswordSharePresence(password, 'decrypt');
5276
- const deserializedKeyShare = JSON.parse(decryptedKeyShare);
5277
- return deserializedKeyShare;
5461
+ // Track whether a user-supplied password was provided so we can emit a
5462
+ // distinct error class on failure. The default `environmentId` fallback is
5463
+ // applied here (and only here) so the original `password` argument retains
5464
+ // the caller's intent — see `recoverEncryptedBackupByWallet` / `getWallet`,
5465
+ // which no longer apply their own fallback.
5466
+ const usedDefaultPassword = !password;
5467
+ const effectivePassword = password != null ? password : this.environmentId;
5468
+ try {
5469
+ const decryptedKeyShare = await decryptData({
5470
+ data: decodedKeyShare,
5471
+ password: effectivePassword
5472
+ });
5473
+ this.logPasswordSharePresence(password, 'decrypt');
5474
+ const deserializedKeyShare = JSON.parse(decryptedKeyShare);
5475
+ return deserializedKeyShare;
5476
+ } catch (error) {
5477
+ // When no user password was supplied, an InvalidPasswordError is
5478
+ // misleading — the user never entered a password, so the "wrong password"
5479
+ // framing doesn't apply. Re-throw as a distinct system-side error so
5480
+ // dashboards/logs classify it as `decryption_failure` instead of
5481
+ // `wrong_password`, and surface that the cause is most likely an
5482
+ // environment-ID mismatch (e.g. wallet encrypted in another environment).
5483
+ if (usedDefaultPassword && error instanceof InvalidPasswordError) {
5484
+ throw new KeyShareDecryptionError({
5485
+ cause: error
5486
+ });
5487
+ }
5488
+ throw error;
5489
+ }
5278
5490
  }
5279
5491
  /**
5280
5492
  * Validates that the provided password is consistent with existing encrypted wallets.
@@ -5530,9 +5742,12 @@ class DynamicWalletClient {
5530
5742
  userId: this.userId
5531
5743
  });
5532
5744
  const dynamicKeyShares = data.keyShares.filter((keyShare)=>keyShare.encryptedAccountCredential !== null && keyShare.backupLocation === BackupLocation.DYNAMIC);
5533
- const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)=>this.decryptKeyShare({
5745
+ const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)=>// `decryptKeyShare` owns the `environmentId` fallback; passing
5746
+ // `password` (possibly undefined) lets it distinguish a user-supplied
5747
+ // password failure from an envId-fallback failure.
5748
+ this.decryptKeyShare({
5534
5749
  keyShare: keyShare.encryptedAccountCredential,
5535
- password: password != null ? password : this.environmentId
5750
+ password
5536
5751
  })));
5537
5752
  if (storeRecoveredShares) {
5538
5753
  await this.setClientKeySharesToStorage({
@@ -6233,9 +6448,11 @@ class DynamicWalletClient {
6233
6448
  return wallet;
6234
6449
  }
6235
6450
  // TODO(zfaizal2): throw error if signedSessionId is not provided after service deploy
6451
+ // `decryptKeyShare` owns the environmentId fallback so the original
6452
+ // `password` (possibly undefined) propagates here.
6236
6453
  const decryptedKeyShares = await this.recoverEncryptedBackupByWallet({
6237
6454
  accountAddress,
6238
- password: password != null ? password : this.environmentId,
6455
+ password,
6239
6456
  walletOperation: walletOperation,
6240
6457
  signedSessionId,
6241
6458
  shareCount
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@dynamic-labs-wallet/browser",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
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.0.2",
7
+ "@dynamic-labs-wallet/core": "1.0.3",
8
8
  "@dynamic-labs-wallet/forward-mpc-client": "0.10.0",
9
- "@dynamic-labs-wallet/primitives": "1.0.2",
9
+ "@dynamic-labs-wallet/primitives": "1.0.3",
10
10
  "@dynamic-labs/sdk-api-core": "^0.0.984",
11
11
  "argon2id": "1.0.1",
12
12
  "axios": "1.15.2",
@@ -4,6 +4,22 @@ export declare const INVALID_PASSWORD_ERROR = "Decryption failed: Invalid passwo
4
4
  export declare class InvalidPasswordError extends Error {
5
5
  constructor();
6
6
  }
7
+ export declare const KEY_SHARE_DECRYPTION_ERROR = "Decryption failed.";
8
+ /**
9
+ * Thrown when a key share cannot be decrypted but no user-supplied password was
10
+ * involved. Distinct from InvalidPasswordError, which is reserved for cases
11
+ * where a user-entered password fails to decrypt a password-encrypted wallet.
12
+ *
13
+ * Internal-only signal: the user-facing message is deliberately generic so it
14
+ * does not leak implementation detail (e.g. environmentId fallback,
15
+ * cross-environment cipher, KDF/version mismatch). The `name` and any attached
16
+ * `cause` carry the real diagnostic for logs / monitoring.
17
+ */
18
+ export declare class KeyShareDecryptionError extends Error {
19
+ constructor(options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
7
23
  /**
8
24
  * Encrypts data using the specified encryption version.
9
25
  * Always uses the latest encryption configuration for new encryptions by default.
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/backup/encryption/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAMpE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGrF,eAAO,MAAM,sBAAsB,mFAAmF,CAAC;AAEvH,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAoBD;;;GAGG;AACH,eAAO,MAAM,WAAW,iCAIrB;IACD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,OAAO,CAAC,aAAa,CA2BxB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,uBAA8B;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAG,OAAO,CAAC,MAAM,CAyDhH,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,YAAa,MAAM,KAAG,kBAmBjE,CAAC"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../../src/backup/encryption/core.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAMpE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAuB,MAAM,YAAY,CAAC;AAGrF,eAAO,MAAM,sBAAsB,mFAAmF,CAAC;AAEvH,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAED,eAAO,MAAM,0BAA0B,uBAAuB,CAAC;AAE/D;;;;;;;;;GASG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAO1C;AAoBD;;;GAGG;AACH,eAAO,MAAM,WAAW,iCAIrB;IACD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,KAAG,OAAO,CAAC,aAAa,CA2BxB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,uBAA8B;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAG,OAAO,CAAC,MAAM,CAyDhH,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,+BAA+B,YAAa,MAAM,KAAG,kBAmBjE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/backup/password/errors.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GACjC,gBAAgB,GAChB,oBAAoB,GACpB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,oBAAoB,GACpB,SAAS,GACT,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6CAA6C;;;;;;;;;CASK,CAAC;AAEhE,eAAO,MAAM,yCAAyC,WAAY,yBAAyB,KAAG,OACvC,CAAC;AAsFxD;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,UAAW,OAAO,KAAG,yBAM5D,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/backup/password/errors.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,MAAM,iBAAiB,GAAG,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GACjC,gBAAgB,GAChB,oBAAoB,GACpB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,oBAAoB,GACpB,SAAS,GACT,SAAS,CAAC;AAEd;;;;;;;;;;GAUG;AACH,eAAO,MAAM,6CAA6C;;;;;;;;;CASK,CAAC;AAEhE,eAAO,MAAM,yCAAyC,WAAY,yBAAyB,KAAG,OACvC,CAAC;AAgGxD;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,UAAW,OAAO,KAAG,yBAM5D,CAAC"}
package/src/client.d.ts CHANGED
@@ -319,12 +319,13 @@ export declare class DynamicWalletClient {
319
319
  * }>} Object containing new and existing client keygen results, IDs and shares
320
320
  * @todo Support higher to lower reshare strategies
321
321
  */
322
- reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, }: {
322
+ reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, delegateToProjectEnvironment, }: {
323
323
  chainName: string;
324
324
  wallet: WalletProperties;
325
325
  accountAddress: string;
326
326
  oldThresholdSignatureScheme: ThresholdSignatureScheme;
327
327
  newThresholdSignatureScheme: ThresholdSignatureScheme;
328
+ delegateToProjectEnvironment?: boolean;
328
329
  }): Promise<{
329
330
  newClientInitKeygenResults: ClientInitKeygenResult[];
330
331
  newClientKeygenIds: string[];
@@ -348,6 +349,7 @@ export declare class DynamicWalletClient {
348
349
  private logOnMalformedCeremonyPayload;
349
350
  private runBackupWithRotationRollback;
350
351
  private rotateRootUserShareSetIdIfChanged;
352
+ private recordDelegatedShareSet;
351
353
  private internalReshare;
352
354
  private performDelegationOperation;
353
355
  delegateKeyShares({ accountAddress, password, signedSessionId, mfaToken, }: {