@dynamic-labs-wallet/browser 0.0.325 → 0.0.327

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.
@@ -352,6 +352,75 @@ class InvalidPasswordError extends Error {
352
352
  };
353
353
 
354
354
  const GOOGLE_DRIVE_UPLOAD_API = 'https://www.googleapis.com';
355
+ const NON_RETRYABLE_AUTH_REASONS = new Set([
356
+ 'insufficientPermissions',
357
+ 'unauthorized',
358
+ 'authError'
359
+ ]);
360
+ const NON_RETRYABLE_AUTH_DETAIL_REASONS = new Set([
361
+ 'ACCESS_TOKEN_SCOPE_INSUFFICIENT',
362
+ 'ACCESS_TOKEN_EXPIRED',
363
+ 'CREDENTIALS_MISSING'
364
+ ]);
365
+ const RATE_LIMIT_REASONS = new Set([
366
+ 'rateLimitExceeded',
367
+ 'userRateLimitExceeded',
368
+ 'dailyLimitExceeded'
369
+ ]);
370
+ const NETWORK_ERROR = {
371
+ message: 'Could not reach Google Drive. Please check your internet connection and try again.',
372
+ isRetryable: true
373
+ };
374
+ /**
375
+ * Maps a Google Drive API error to an actionable user-facing message and a retry hint.
376
+ * The retry hint is consumed by retryPromise to short-circuit on non-transient failures.
377
+ */ const mapGoogleDriveUploadError = (httpStatus, body)=>{
378
+ var _error_errors_, _error_errors, _error_details_, _error_details;
379
+ const error = body == null ? void 0 : body.error;
380
+ const reason = error == null ? void 0 : (_error_errors = error.errors) == null ? void 0 : (_error_errors_ = _error_errors[0]) == null ? void 0 : _error_errors_.reason;
381
+ const detailReason = error == null ? void 0 : (_error_details = error.details) == null ? void 0 : (_error_details_ = _error_details[0]) == null ? void 0 : _error_details_.reason;
382
+ if (httpStatus === 401 || reason && NON_RETRYABLE_AUTH_REASONS.has(reason) || detailReason && NON_RETRYABLE_AUTH_DETAIL_REASONS.has(detailReason)) {
383
+ return {
384
+ message: 'Google Drive access denied: missing or insufficient permissions. Make sure the app is requesting Drive permission and that you allowed it during sign-in, then try again.',
385
+ isRetryable: false
386
+ };
387
+ }
388
+ if (reason === 'storageQuotaExceeded') {
389
+ return {
390
+ message: 'Google Drive storage is full. Free up space in your Google Drive and try again.',
391
+ isRetryable: false
392
+ };
393
+ }
394
+ if (httpStatus === 429 || reason && RATE_LIMIT_REASONS.has(reason)) {
395
+ return {
396
+ message: 'Google Drive is temporarily rate-limited. Please try again in a few moments.',
397
+ isRetryable: true
398
+ };
399
+ }
400
+ if (httpStatus >= 500 && httpStatus < 600) {
401
+ return {
402
+ message: 'Google Drive is temporarily unavailable. Please try again shortly.',
403
+ isRetryable: true
404
+ };
405
+ }
406
+ const detail = reason != null ? reason : error == null ? void 0 : error.status;
407
+ return {
408
+ message: detail ? `Google Drive upload failed (HTTP ${httpStatus}: ${detail}).` : `Google Drive upload failed (HTTP ${httpStatus}).`,
409
+ isRetryable: false
410
+ };
411
+ };
412
+ const parseGoogleDriveErrorBody = async (response)=>{
413
+ try {
414
+ return await response.json();
415
+ } catch (e) {
416
+ return null;
417
+ }
418
+ };
419
+ const createGoogleDriveError = (mapped)=>{
420
+ const error = new Error(mapped.message);
421
+ error.isRetryable = mapped.isRetryable;
422
+ return error;
423
+ };
355
424
  const uploadFileToGoogleDriveAppStorage = async ({ accessToken, fileName, jsonData })=>{
356
425
  return uploadFileToGoogleDrive({
357
426
  accessToken,
@@ -373,19 +442,24 @@ const uploadFileToGoogleDrivePersonal = async ({ accessToken, fileName, jsonData
373
442
  });
374
443
  };
375
444
  /**
376
- * Verifies that a file exists on Google Drive by fetching its metadata
377
- */ const verifyUpload = async ({ accessToken, fileId })=>{
378
- const verifyResponse = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files/${fileId}?fields=id,name`, {
445
+ * Verifies that a file exists on Google Drive by listing files
446
+ * Uses appropriate space parameter based on upload location
447
+ */ const verifyUpload = async ({ accessToken, fileId, fileName, isAppDataFolder })=>{
448
+ var _verifyResult_files;
449
+ const spacesParam = isAppDataFolder ? "appDataFolder" : "drive";
450
+ const nameQuery = `name='${fileName}'`;
451
+ const verifyResponse = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(nameQuery)}&spaces=${spacesParam}&fields=files(id,name)`, {
379
452
  headers: {
380
453
  Authorization: `Bearer ${accessToken}`
381
454
  }
382
455
  });
383
456
  if (!verifyResponse.ok) {
384
- throw new Error(`Upload verification failed: file ${fileId} not accessible after upload`);
457
+ throw new Error(`Upload verification failed: could not list files in ${spacesParam}`);
385
458
  }
386
459
  const verifyResult = await verifyResponse.json();
387
- if (verifyResult.id !== fileId) {
388
- throw new Error(`Upload verification failed: expected file ID ${fileId}, got ${verifyResult.id}`);
460
+ const uploadedFile = (_verifyResult_files = verifyResult.files) == null ? void 0 : _verifyResult_files.find((f)=>f.id === fileId);
461
+ if (!uploadedFile) {
462
+ throw new Error(`Upload verification failed: file ${fileId} not found in ${spacesParam}`);
389
463
  }
390
464
  };
391
465
  const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parents })=>{
@@ -411,9 +485,12 @@ const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parent
411
485
  Authorization: `Bearer ${accessToken}`
412
486
  },
413
487
  body: form
488
+ }).catch(()=>{
489
+ throw createGoogleDriveError(NETWORK_ERROR);
414
490
  });
415
491
  if (!response.ok) {
416
- throw new Error('Error uploading file');
492
+ const body = await parseGoogleDriveErrorBody(response);
493
+ throw createGoogleDriveError(mapGoogleDriveUploadError(response.status, body));
417
494
  }
418
495
  const result = await response.json();
419
496
  const fileId = result.id;
@@ -422,13 +499,16 @@ const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parent
422
499
  }
423
500
  await verifyUpload({
424
501
  accessToken,
425
- fileId
502
+ fileId,
503
+ fileName,
504
+ isAppDataFolder: parents.includes('appDataFolder')
426
505
  });
427
506
  return result; // Return file metadata, including file ID
428
507
  };
429
508
  const listFilesFromGoogleDrive = async ({ accessToken, fileName })=>{
430
- // Step 1: List all files inside `appDataFolder` with the specified backup filename
431
- const resp = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(`name='${fileName}'`)}&spaces=appDataFolder&orderBy=createdTime desc`, {
509
+ // List all files inside appDataFolder with the specified backup filename
510
+ const nameQuery = `name='${fileName}'`;
511
+ const resp = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(nameQuery)}&spaces=${"appDataFolder"}&orderBy=createdTime desc`, {
432
512
  headers: {
433
513
  Authorization: `Bearer ${accessToken}`
434
514
  }
@@ -592,11 +672,11 @@ const logRetrySuccess = (operationName, attempts, logContext)=>{
592
672
  attemptsTaken: attempts + 1
593
673
  }));
594
674
  };
595
- const logRetryFailure = (operationName, attempts, maxAttempts, errorContext, logContext)=>{
675
+ const logRetryFailure = (operationName, attempts, maxAttempts, errorContext, logContext, isNonRetryable = false)=>{
596
676
  core.Logger.warn(`Failed to execute ${operationName} on attempt ${attempts}/${maxAttempts}`, _extends({}, logContext, errorContext, {
597
677
  attempt: attempts,
598
678
  maxAttempts,
599
- willRetry: attempts < maxAttempts
679
+ willRetry: !isNonRetryable && attempts < maxAttempts
600
680
  }));
601
681
  };
602
682
  const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)=>{
@@ -623,7 +703,12 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
623
703
  } catch (error) {
624
704
  attempts++;
625
705
  const errorContext = buildErrorContext(error);
626
- logRetryFailure(operationName, attempts, maxAttempts, errorContext, logContext);
706
+ const isNonRetryable = (error == null ? void 0 : error.isRetryable) === false;
707
+ logRetryFailure(operationName, attempts, maxAttempts, errorContext, logContext, isNonRetryable);
708
+ if (isNonRetryable) {
709
+ core.Logger.debug(`Skipping retry for ${operationName}: error marked non-retryable`, _extends({}, logContext, errorContext));
710
+ throw error;
711
+ }
627
712
  if (attempts === maxAttempts) {
628
713
  logRetryExhausted(operationName, maxAttempts, errorContext, logContext);
629
714
  throw error;
@@ -739,6 +824,27 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
739
824
  }
740
825
  return false;
741
826
  };
827
+ /**
828
+ * Determines whether a reshare operation should reuse the wallet's existing backup locations.
829
+ *
830
+ * **Why this exists:**
831
+ * When `reshareOnNextSignOn` is triggered (e.g., after Google OAuth refresh), the reshare happens
832
+ * automatically without the caller specifying backup parameters. Without this detection, the reshare
833
+ * would proceed with empty backup locations, potentially losing the user's Google Drive or
834
+ * delegation backup configuration.
835
+ *
836
+ * **When it returns true:**
837
+ * - Same threshold (not upgrading/downgrading the signature scheme)
838
+ * - No cloud providers explicitly passed (caller didn't request backup changes)
839
+ * - No delegation explicitly passed (caller didn't request delegation changes)
840
+ *
841
+ * When true, the reshare will auto-populate backup locations from the wallet's existing
842
+ * `clientKeySharesBackupInfo` state, ensuring backups are preserved.
843
+ */ const shouldReshareToSameBackups = ({ oldThresholdSignatureScheme, newThresholdSignatureScheme, cloudProviders = [], delegateToProjectEnvironment = false })=>{
844
+ const isSameThreshold = oldThresholdSignatureScheme === newThresholdSignatureScheme;
845
+ const noBackupChangesRequested = cloudProviders.length === 0 && !delegateToProjectEnvironment;
846
+ return isSameThreshold && noBackupChangesRequested;
847
+ };
742
848
 
743
849
  const CLOUDKIT_CDN_URL = 'https://cdn.apple-cloudkit.com/ck/2/cloudkit.js';
744
850
  const BACKUP_RECORD_TYPE = 'Backup';
@@ -1124,18 +1230,24 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1124
1230
  };
1125
1231
  /**
1126
1232
  * Processes a single upload result and logs appropriately
1127
- * @returns Error message if failed, undefined if successful
1233
+ * @returns Failure details if rejected, undefined if successful
1128
1234
  */ const processUploadResult = (result, locationName, logContext, logger)=>{
1235
+ var _result_reason;
1129
1236
  if (result.status === 'fulfilled') {
1130
1237
  logger.info(`[DynamicWaasWalletClient] Successfully uploaded keyshares to ${locationName}`, logContext);
1131
1238
  return undefined;
1132
1239
  }
1133
1240
  const { message, stack } = getErrorDetails(result.reason);
1241
+ const isRetryable = ((_result_reason = result.reason) == null ? void 0 : _result_reason.isRetryable) !== false;
1134
1242
  logger.error(`[DynamicWaasWalletClient] Failed to upload keyshares to ${locationName}`, _extends({}, logContext, {
1135
1243
  error: message,
1136
1244
  errorStack: stack
1137
1245
  }));
1138
- return `Failed to backup keyshares to ${locationName}: ${message}`;
1246
+ return {
1247
+ locationName,
1248
+ message,
1249
+ isRetryable
1250
+ };
1139
1251
  };
1140
1252
  /**
1141
1253
  * Uploads a backup to Google Drive App
@@ -1175,16 +1287,27 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1175
1287
  })
1176
1288
  ];
1177
1289
  const results = await Promise.allSettled(uploadPromises);
1178
- const errors = [
1290
+ const failures = [
1179
1291
  processUploadResult(results[0], 'Google Drive App Storage', logContext, logger),
1180
1292
  processUploadResult(results[1], 'Google Drive Personal', logContext, logger)
1181
- ].filter((error)=>error !== undefined);
1182
- if (errors.length > 0) {
1293
+ ].filter((failure)=>failure !== undefined);
1294
+ if (failures.length > 0) {
1183
1295
  logger.error('[DynamicWaasWalletClient] Google Drive backup failed', _extends({}, logContext, {
1184
- errorCount: errors.length,
1185
- errors
1296
+ errorCount: failures.length,
1297
+ errors: failures.map((f)=>`Failed to backup keyshares to ${f.locationName}: ${f.message}`)
1186
1298
  }));
1187
- throw new Error(`[DynamicWaasWalletClient] ${errors.join('; ')}`);
1299
+ // Both upload destinations (appDataFolder + personal) commonly fail with
1300
+ // the same underlying error (e.g., missing OAuth scope). When all failures
1301
+ // share one message, surface it once without per-location wrapping;
1302
+ // otherwise keep a short "Location: message" prefix per failure so the
1303
+ // user can see what differed.
1304
+ const uniqueMessages = [
1305
+ ...new Set(failures.map((f)=>f.message))
1306
+ ];
1307
+ const finalMessage = uniqueMessages.length === 1 ? uniqueMessages[0] : failures.map((f)=>`${f.locationName}: ${f.message}`).join(' | ');
1308
+ const aggregatedError = new Error(finalMessage);
1309
+ aggregatedError.isRetryable = failures.every((f)=>f.isRetryable);
1310
+ throw aggregatedError;
1188
1311
  }
1189
1312
  logger.info('[DynamicWaasWalletClient] Google Drive backup completed successfully', logContext);
1190
1313
  };
@@ -2102,18 +2225,33 @@ class DynamicWalletClient {
2102
2225
  }
2103
2226
  }
2104
2227
  async clientKeyGen({ chainName, roomId, serverKeygenIds, clientKeygenInitResults, thresholdSignatureScheme, bitcoinConfig, dynamicRequestId, traceContext }) {
2228
+ // Try forward MPC first if enabled
2105
2229
  if (this.forwardMPCEnabled) {
2106
- return this.forwardMPCClientKeygen({
2107
- chainName,
2108
- roomId,
2109
- serverKeygenIds,
2110
- clientKeygenInitResults,
2111
- thresholdSignatureScheme,
2112
- bitcoinConfig,
2113
- dynamicRequestId,
2114
- traceContext
2115
- });
2230
+ try {
2231
+ return await this.forwardMPCClientKeygen({
2232
+ chainName,
2233
+ roomId,
2234
+ serverKeygenIds,
2235
+ clientKeygenInitResults,
2236
+ thresholdSignatureScheme,
2237
+ bitcoinConfig,
2238
+ dynamicRequestId,
2239
+ traceContext
2240
+ });
2241
+ } catch (error) {
2242
+ const errorInfo = core.classifyForwardMpcError(error);
2243
+ this.logger.warn('Forward MPC keygen failed', _extends({}, errorInfo, {
2244
+ chainName,
2245
+ environmentId: this.environmentId,
2246
+ dynamicRequestId
2247
+ }));
2248
+ if (!errorInfo.shouldFallback) {
2249
+ throw error;
2250
+ }
2251
+ // Fall through to relay-based keygen below
2252
+ }
2116
2253
  }
2254
+ // Relay-based keygen (fallback or when forward MPC is disabled)
2117
2255
  // Get the chain config and the mpc signer
2118
2256
  const mpcSigner = getMPCSigner({
2119
2257
  chainName,
@@ -2431,20 +2569,35 @@ class DynamicWalletClient {
2431
2569
  derivationPath,
2432
2570
  isFormatted
2433
2571
  }, this.getTraceContext(traceContext)));
2572
+ // Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
2434
2573
  if (this.forwardMPCEnabled) {
2435
- return this.forwardMPCClientSign({
2436
- chainName,
2437
- message,
2438
- roomId,
2439
- keyShare,
2440
- derivationPath,
2441
- formattedMessage,
2442
- dynamicRequestId,
2443
- isFormatted,
2444
- traceContext,
2445
- bitcoinConfig
2446
- });
2574
+ try {
2575
+ return await this.forwardMPCClientSign({
2576
+ chainName,
2577
+ message,
2578
+ roomId,
2579
+ keyShare,
2580
+ derivationPath,
2581
+ formattedMessage,
2582
+ dynamicRequestId,
2583
+ isFormatted,
2584
+ traceContext,
2585
+ bitcoinConfig
2586
+ });
2587
+ } catch (error) {
2588
+ const errorInfo = core.classifyForwardMpcError(error);
2589
+ this.logger.warn('Forward MPC signing failed', _extends({}, errorInfo, {
2590
+ chainName,
2591
+ environmentId: this.environmentId,
2592
+ dynamicRequestId
2593
+ }));
2594
+ if (!errorInfo.shouldFallback) {
2595
+ throw error;
2596
+ }
2597
+ // Fall through to relay-based signing below
2598
+ }
2447
2599
  }
2600
+ // Relay-based signing (fallback or when forward MPC is disabled)
2448
2601
  // Unwrap MessageHash for BIP340 as it expects Uint8Array or hex string
2449
2602
  let messageToSign = formattedMessage;
2450
2603
  if (mpcSigner instanceof web.BIP340 && formattedMessage instanceof web.MessageHash) {
@@ -2691,6 +2844,52 @@ class DynamicWalletClient {
2691
2844
  }));
2692
2845
  }
2693
2846
  /**
2847
+ * Resolves backup locations for a reshare operation.
2848
+ *
2849
+ * When reshareOnNextSignOn triggers (e.g., after Google OAuth refresh), the caller
2850
+ * doesn't pass backup parameters. This method detects that case and preserves the
2851
+ * wallet's existing backup locations (Google Drive, iCloud, delegation) instead of
2852
+ * proceeding with empty backup locations.
2853
+ *
2854
+ * @returns The backup locations to use for the reshare operation
2855
+ */ resolveBackupLocationsForReshare({ oldThresholdSignatureScheme, newThresholdSignatureScheme, cloudProviders = [], delegateToProjectEnvironment = false, accountAddress, dynamicRequestId }) {
2856
+ const shouldPreserveExisting = shouldReshareToSameBackups({
2857
+ oldThresholdSignatureScheme,
2858
+ newThresholdSignatureScheme,
2859
+ cloudProviders,
2860
+ delegateToProjectEnvironment
2861
+ });
2862
+ if (!shouldPreserveExisting) {
2863
+ return {
2864
+ cloudProviders,
2865
+ delegateToProjectEnvironment
2866
+ };
2867
+ }
2868
+ // Wallet is guaranteed to be in map - verifyPassword calls requireWalletFromMap upstream
2869
+ const walletProperties = this.getWalletFromMap(accountAddress);
2870
+ if (!walletProperties) {
2871
+ this.logger.warn('[resolveBackupLocationsForReshare] Wallet not in map, using passed params', {
2872
+ dynamicRequestId,
2873
+ accountAddress
2874
+ });
2875
+ return {
2876
+ cloudProviders,
2877
+ delegateToProjectEnvironment
2878
+ };
2879
+ }
2880
+ const preservedCloudProviders = getActiveCloudProviders(walletProperties.clientKeySharesBackupInfo);
2881
+ const preservedDelegation = hasDelegatedBackup(walletProperties.clientKeySharesBackupInfo);
2882
+ this.logger.info('[resolveBackupLocationsForReshare] Preserving existing backup locations', {
2883
+ dynamicRequestId,
2884
+ cloudProviders: preservedCloudProviders,
2885
+ delegateToProjectEnvironment: preservedDelegation
2886
+ });
2887
+ return {
2888
+ cloudProviders: preservedCloudProviders,
2889
+ delegateToProjectEnvironment: preservedDelegation
2890
+ };
2891
+ }
2892
+ /**
2694
2893
  * Gets the Bitcoin config for MPC operations by looking up addressType
2695
2894
  * from walletMap, with fallback to deriving it from derivationPath.
2696
2895
  */ getBitcoinConfigForChain(chainName, accountAddress) {
@@ -2907,6 +3106,8 @@ class DynamicWalletClient {
2907
3106
  }
2908
3107
  async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false }) {
2909
3108
  const dynamicRequestId = uuid.v4();
3109
+ // Password validation - wrapped in try-catch for consistent error handling
3110
+ // This path should NOT wipe key shares on failure (shares remain valid)
2910
3111
  try {
2911
3112
  this.assertPasswordRequired(password);
2912
3113
  await this.verifyPassword({
@@ -2919,6 +3120,30 @@ class DynamicWalletClient {
2919
3120
  password,
2920
3121
  signedSessionId
2921
3122
  });
3123
+ } catch (error) {
3124
+ // Log password validation error but do NOT wipe key shares
3125
+ logError({
3126
+ message: '[DynamicWaasWalletClient]: Password validation failed during reshare',
3127
+ error: error,
3128
+ context: {
3129
+ accountAddress,
3130
+ dynamicRequestId
3131
+ }
3132
+ });
3133
+ throw error;
3134
+ }
3135
+ // Resolve backup locations for this reshare - may preserve existing locations
3136
+ // when reshareOnNextSignOn triggers without explicit backup parameters
3137
+ const { cloudProviders: resolvedCloudProviders, delegateToProjectEnvironment: resolvedDelegation } = this.resolveBackupLocationsForReshare({
3138
+ oldThresholdSignatureScheme,
3139
+ newThresholdSignatureScheme,
3140
+ cloudProviders,
3141
+ delegateToProjectEnvironment,
3142
+ accountAddress,
3143
+ dynamicRequestId
3144
+ });
3145
+ // MPC ceremony path - errors here SHOULD wipe shares as they may be inconsistent
3146
+ try {
2922
3147
  const { existingClientShareCount } = core.getReshareConfig({
2923
3148
  oldThresholdSignatureScheme,
2924
3149
  newThresholdSignatureScheme
@@ -2953,7 +3178,7 @@ class DynamicWalletClient {
2953
3178
  oldThresholdSignatureScheme,
2954
3179
  newThresholdSignatureScheme,
2955
3180
  dynamicRequestId,
2956
- delegateToProjectEnvironment,
3181
+ delegateToProjectEnvironment: resolvedDelegation,
2957
3182
  mfaToken,
2958
3183
  elevatedAccessToken,
2959
3184
  revokeDelegation
@@ -3046,6 +3271,7 @@ class DynamicWalletClient {
3046
3271
  dynamicRequestId
3047
3272
  });
3048
3273
  // Retry the entire reshare operation (need new room and keygen IDs from server)
3274
+ // Pass effective* values so retry uses same backup locations
3049
3275
  return this.internalReshare({
3050
3276
  chainName,
3051
3277
  accountAddress,
@@ -3053,8 +3279,8 @@ class DynamicWalletClient {
3053
3279
  newThresholdSignatureScheme,
3054
3280
  password,
3055
3281
  signedSessionId,
3056
- cloudProviders,
3057
- delegateToProjectEnvironment,
3282
+ cloudProviders: resolvedCloudProviders,
3283
+ delegateToProjectEnvironment: resolvedDelegation,
3058
3284
  mfaToken,
3059
3285
  elevatedAccessToken,
3060
3286
  revokeDelegation,
@@ -3069,26 +3295,27 @@ class DynamicWalletClient {
3069
3295
  ];
3070
3296
  let distribution;
3071
3297
  // Generic distribution logic - works with any cloud providers
3072
- if (delegateToProjectEnvironment && cloudProviders.length > 0) {
3298
+ // Use effective* variables which may have been populated from existing wallet state
3299
+ if (resolvedDelegation && resolvedCloudProviders.length > 0) {
3073
3300
  // Delegation + Cloud Providers: Client's existing share backs up to both Dynamic and cloud providers.
3074
3301
  // The new share goes to the webhook for delegation.
3075
3302
  distribution = createDelegationWithCloudProviderDistribution({
3076
- providers: cloudProviders,
3303
+ providers: resolvedCloudProviders,
3077
3304
  existingShares: existingReshareResults,
3078
3305
  delegatedShare: newReshareResults[0]
3079
3306
  });
3080
- } else if (delegateToProjectEnvironment) {
3307
+ } else if (resolvedDelegation) {
3081
3308
  // Delegation only: Client's existing share backs up to Dynamic.
3082
3309
  // The new share goes to the webhook for delegation. No cloud provider backup.
3083
3310
  distribution = createDelegationOnlyDistribution({
3084
3311
  existingShares: existingReshareResults,
3085
3312
  delegatedShare: newReshareResults[0]
3086
3313
  });
3087
- } else if (cloudProviders.length > 0) {
3314
+ } else if (resolvedCloudProviders.length > 0) {
3088
3315
  // Cloud Providers only: Split shares between Dynamic (N-1) and cloud providers (1).
3089
3316
  // The last share (new share) goes to cloud providers.
3090
3317
  distribution = createCloudProviderDistribution({
3091
- providers: cloudProviders,
3318
+ providers: resolvedCloudProviders,
3092
3319
  allShares: allClientShares
3093
3320
  });
3094
3321
  } else {
@@ -3126,6 +3353,8 @@ class DynamicWalletClient {
3126
3353
  oldThresholdSignatureScheme,
3127
3354
  newThresholdSignatureScheme,
3128
3355
  cloudProviders,
3356
+ resolvedCloudProviders,
3357
+ resolvedDelegation,
3129
3358
  dynamicRequestId
3130
3359
  }
3131
3360
  });
@@ -4169,12 +4398,14 @@ class DynamicWalletClient {
4169
4398
  const oauthAccountId = getGoogleOAuthAccountId(user == null ? void 0 : user.verifiedCredentials);
4170
4399
  if (!oauthAccountId) {
4171
4400
  const error = new Error('No Google OAuth account ID found');
4401
+ // PII: intentionally omitting full user object
4172
4402
  logError({
4173
4403
  message: 'No Google OAuth account ID found',
4174
4404
  error,
4175
4405
  context: {
4176
- user,
4177
- accountAddress
4406
+ accountAddress,
4407
+ userId: user == null ? void 0 : user.id,
4408
+ projectEnvironmentId: user == null ? void 0 : user.projectEnvironmentId
4178
4409
  }
4179
4410
  });
4180
4411
  throw error;
@@ -5553,6 +5784,7 @@ exports.listICloudBackups = listICloudBackups;
5553
5784
  exports.mergeUniqueKeyShares = mergeUniqueKeyShares;
5554
5785
  exports.readEnvironmentSettings = readEnvironmentSettings;
5555
5786
  exports.retryPromise = retryPromise;
5787
+ exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
5556
5788
  exports.timeoutPromise = timeoutPromise;
5557
5789
  Object.keys(core).forEach(function (k) {
5558
5790
  if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {