@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.
package/index.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BitcoinAddressType, SigningAlgorithm, MPC_RELAY_PROD_API_URL, getMPCChainConfig, BackupLocation, ENCRYPTED_SHARES_STORAGE_SUFFIX, Logger, handleAxiosError, WalletOperation, WalletReadyState, parseNamespacedVersion, FEATURE_FLAGS, ThresholdSignatureScheme, getClientThreshold, MPC_CONFIG, getTSSConfig, serializeMessageForForwardMPC, getReshareConfig, getRequiredExternalKeyShareId, verifiedCredentialNameToChainEnum, AuthMode, NoopLogger, DynamicApiClient, getEnvironmentFromUrl, IFRAME_DOMAIN_MAP } from '@dynamic-labs-wallet/core';
1
+ import { BitcoinAddressType, SigningAlgorithm, MPC_RELAY_PROD_API_URL, getMPCChainConfig, BackupLocation, ENCRYPTED_SHARES_STORAGE_SUFFIX, Logger, handleAxiosError, WalletOperation, WalletReadyState, parseNamespacedVersion, FEATURE_FLAGS, ThresholdSignatureScheme, getClientThreshold, MPC_CONFIG, classifyForwardMpcError, getTSSConfig, serializeMessageForForwardMPC, getReshareConfig, getRequiredExternalKeyShareId, verifiedCredentialNameToChainEnum, AuthMode, NoopLogger, DynamicApiClient, 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 { BIP340, ExportableEd25519, Ecdsa, MessageHash, EcdsaSignature, EcdsaKeygenResult, ExportableEd25519KeygenResult, BIP340KeygenResult } from '#internal/web';
@@ -353,6 +353,75 @@ class InvalidPasswordError extends Error {
353
353
  };
354
354
 
355
355
  const GOOGLE_DRIVE_UPLOAD_API = 'https://www.googleapis.com';
356
+ const NON_RETRYABLE_AUTH_REASONS = new Set([
357
+ 'insufficientPermissions',
358
+ 'unauthorized',
359
+ 'authError'
360
+ ]);
361
+ const NON_RETRYABLE_AUTH_DETAIL_REASONS = new Set([
362
+ 'ACCESS_TOKEN_SCOPE_INSUFFICIENT',
363
+ 'ACCESS_TOKEN_EXPIRED',
364
+ 'CREDENTIALS_MISSING'
365
+ ]);
366
+ const RATE_LIMIT_REASONS = new Set([
367
+ 'rateLimitExceeded',
368
+ 'userRateLimitExceeded',
369
+ 'dailyLimitExceeded'
370
+ ]);
371
+ const NETWORK_ERROR = {
372
+ message: 'Could not reach Google Drive. Please check your internet connection and try again.',
373
+ isRetryable: true
374
+ };
375
+ /**
376
+ * Maps a Google Drive API error to an actionable user-facing message and a retry hint.
377
+ * The retry hint is consumed by retryPromise to short-circuit on non-transient failures.
378
+ */ const mapGoogleDriveUploadError = (httpStatus, body)=>{
379
+ var _error_errors_, _error_errors, _error_details_, _error_details;
380
+ const error = body == null ? void 0 : body.error;
381
+ const reason = error == null ? void 0 : (_error_errors = error.errors) == null ? void 0 : (_error_errors_ = _error_errors[0]) == null ? void 0 : _error_errors_.reason;
382
+ const detailReason = error == null ? void 0 : (_error_details = error.details) == null ? void 0 : (_error_details_ = _error_details[0]) == null ? void 0 : _error_details_.reason;
383
+ if (httpStatus === 401 || reason && NON_RETRYABLE_AUTH_REASONS.has(reason) || detailReason && NON_RETRYABLE_AUTH_DETAIL_REASONS.has(detailReason)) {
384
+ return {
385
+ 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.',
386
+ isRetryable: false
387
+ };
388
+ }
389
+ if (reason === 'storageQuotaExceeded') {
390
+ return {
391
+ message: 'Google Drive storage is full. Free up space in your Google Drive and try again.',
392
+ isRetryable: false
393
+ };
394
+ }
395
+ if (httpStatus === 429 || reason && RATE_LIMIT_REASONS.has(reason)) {
396
+ return {
397
+ message: 'Google Drive is temporarily rate-limited. Please try again in a few moments.',
398
+ isRetryable: true
399
+ };
400
+ }
401
+ if (httpStatus >= 500 && httpStatus < 600) {
402
+ return {
403
+ message: 'Google Drive is temporarily unavailable. Please try again shortly.',
404
+ isRetryable: true
405
+ };
406
+ }
407
+ const detail = reason != null ? reason : error == null ? void 0 : error.status;
408
+ return {
409
+ message: detail ? `Google Drive upload failed (HTTP ${httpStatus}: ${detail}).` : `Google Drive upload failed (HTTP ${httpStatus}).`,
410
+ isRetryable: false
411
+ };
412
+ };
413
+ const parseGoogleDriveErrorBody = async (response)=>{
414
+ try {
415
+ return await response.json();
416
+ } catch (e) {
417
+ return null;
418
+ }
419
+ };
420
+ const createGoogleDriveError = (mapped)=>{
421
+ const error = new Error(mapped.message);
422
+ error.isRetryable = mapped.isRetryable;
423
+ return error;
424
+ };
356
425
  const uploadFileToGoogleDriveAppStorage = async ({ accessToken, fileName, jsonData })=>{
357
426
  return uploadFileToGoogleDrive({
358
427
  accessToken,
@@ -374,19 +443,24 @@ const uploadFileToGoogleDrivePersonal = async ({ accessToken, fileName, jsonData
374
443
  });
375
444
  };
376
445
  /**
377
- * Verifies that a file exists on Google Drive by fetching its metadata
378
- */ const verifyUpload = async ({ accessToken, fileId })=>{
379
- const verifyResponse = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files/${fileId}?fields=id,name`, {
446
+ * Verifies that a file exists on Google Drive by listing files
447
+ * Uses appropriate space parameter based on upload location
448
+ */ const verifyUpload = async ({ accessToken, fileId, fileName, isAppDataFolder })=>{
449
+ var _verifyResult_files;
450
+ const spacesParam = isAppDataFolder ? "appDataFolder" : "drive";
451
+ const nameQuery = `name='${fileName}'`;
452
+ const verifyResponse = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(nameQuery)}&spaces=${spacesParam}&fields=files(id,name)`, {
380
453
  headers: {
381
454
  Authorization: `Bearer ${accessToken}`
382
455
  }
383
456
  });
384
457
  if (!verifyResponse.ok) {
385
- throw new Error(`Upload verification failed: file ${fileId} not accessible after upload`);
458
+ throw new Error(`Upload verification failed: could not list files in ${spacesParam}`);
386
459
  }
387
460
  const verifyResult = await verifyResponse.json();
388
- if (verifyResult.id !== fileId) {
389
- throw new Error(`Upload verification failed: expected file ID ${fileId}, got ${verifyResult.id}`);
461
+ const uploadedFile = (_verifyResult_files = verifyResult.files) == null ? void 0 : _verifyResult_files.find((f)=>f.id === fileId);
462
+ if (!uploadedFile) {
463
+ throw new Error(`Upload verification failed: file ${fileId} not found in ${spacesParam}`);
390
464
  }
391
465
  };
392
466
  const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parents })=>{
@@ -412,9 +486,12 @@ const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parent
412
486
  Authorization: `Bearer ${accessToken}`
413
487
  },
414
488
  body: form
489
+ }).catch(()=>{
490
+ throw createGoogleDriveError(NETWORK_ERROR);
415
491
  });
416
492
  if (!response.ok) {
417
- throw new Error('Error uploading file');
493
+ const body = await parseGoogleDriveErrorBody(response);
494
+ throw createGoogleDriveError(mapGoogleDriveUploadError(response.status, body));
418
495
  }
419
496
  const result = await response.json();
420
497
  const fileId = result.id;
@@ -423,13 +500,16 @@ const uploadFileToGoogleDrive = async ({ accessToken, fileName, jsonData, parent
423
500
  }
424
501
  await verifyUpload({
425
502
  accessToken,
426
- fileId
503
+ fileId,
504
+ fileName,
505
+ isAppDataFolder: parents.includes('appDataFolder')
427
506
  });
428
507
  return result; // Return file metadata, including file ID
429
508
  };
430
509
  const listFilesFromGoogleDrive = async ({ accessToken, fileName })=>{
431
- // Step 1: List all files inside `appDataFolder` with the specified backup filename
432
- const resp = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(`name='${fileName}'`)}&spaces=appDataFolder&orderBy=createdTime desc`, {
510
+ // List all files inside appDataFolder with the specified backup filename
511
+ const nameQuery = `name='${fileName}'`;
512
+ const resp = await fetch(`${GOOGLE_DRIVE_UPLOAD_API}/drive/v3/files?q=${encodeURIComponent(nameQuery)}&spaces=${"appDataFolder"}&orderBy=createdTime desc`, {
433
513
  headers: {
434
514
  Authorization: `Bearer ${accessToken}`
435
515
  }
@@ -593,11 +673,11 @@ const logRetrySuccess = (operationName, attempts, logContext)=>{
593
673
  attemptsTaken: attempts + 1
594
674
  }));
595
675
  };
596
- const logRetryFailure = (operationName, attempts, maxAttempts, errorContext, logContext)=>{
676
+ const logRetryFailure = (operationName, attempts, maxAttempts, errorContext, logContext, isNonRetryable = false)=>{
597
677
  Logger.warn(`Failed to execute ${operationName} on attempt ${attempts}/${maxAttempts}`, _extends({}, logContext, errorContext, {
598
678
  attempt: attempts,
599
679
  maxAttempts,
600
- willRetry: attempts < maxAttempts
680
+ willRetry: !isNonRetryable && attempts < maxAttempts
601
681
  }));
602
682
  };
603
683
  const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)=>{
@@ -624,7 +704,12 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
624
704
  } catch (error) {
625
705
  attempts++;
626
706
  const errorContext = buildErrorContext(error);
627
- logRetryFailure(operationName, attempts, maxAttempts, errorContext, logContext);
707
+ const isNonRetryable = (error == null ? void 0 : error.isRetryable) === false;
708
+ logRetryFailure(operationName, attempts, maxAttempts, errorContext, logContext, isNonRetryable);
709
+ if (isNonRetryable) {
710
+ Logger.debug(`Skipping retry for ${operationName}: error marked non-retryable`, _extends({}, logContext, errorContext));
711
+ throw error;
712
+ }
628
713
  if (attempts === maxAttempts) {
629
714
  logRetryExhausted(operationName, maxAttempts, errorContext, logContext);
630
715
  throw error;
@@ -740,6 +825,27 @@ const downloadStringAsFile = ({ filename, content, mimeType = 'application/json'
740
825
  }
741
826
  return false;
742
827
  };
828
+ /**
829
+ * Determines whether a reshare operation should reuse the wallet's existing backup locations.
830
+ *
831
+ * **Why this exists:**
832
+ * When `reshareOnNextSignOn` is triggered (e.g., after Google OAuth refresh), the reshare happens
833
+ * automatically without the caller specifying backup parameters. Without this detection, the reshare
834
+ * would proceed with empty backup locations, potentially losing the user's Google Drive or
835
+ * delegation backup configuration.
836
+ *
837
+ * **When it returns true:**
838
+ * - Same threshold (not upgrading/downgrading the signature scheme)
839
+ * - No cloud providers explicitly passed (caller didn't request backup changes)
840
+ * - No delegation explicitly passed (caller didn't request delegation changes)
841
+ *
842
+ * When true, the reshare will auto-populate backup locations from the wallet's existing
843
+ * `clientKeySharesBackupInfo` state, ensuring backups are preserved.
844
+ */ const shouldReshareToSameBackups = ({ oldThresholdSignatureScheme, newThresholdSignatureScheme, cloudProviders = [], delegateToProjectEnvironment = false })=>{
845
+ const isSameThreshold = oldThresholdSignatureScheme === newThresholdSignatureScheme;
846
+ const noBackupChangesRequested = cloudProviders.length === 0 && !delegateToProjectEnvironment;
847
+ return isSameThreshold && noBackupChangesRequested;
848
+ };
743
849
 
744
850
  const CLOUDKIT_CDN_URL = 'https://cdn.apple-cloudkit.com/ck/2/cloudkit.js';
745
851
  const BACKUP_RECORD_TYPE = 'Backup';
@@ -1125,18 +1231,24 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1125
1231
  };
1126
1232
  /**
1127
1233
  * Processes a single upload result and logs appropriately
1128
- * @returns Error message if failed, undefined if successful
1234
+ * @returns Failure details if rejected, undefined if successful
1129
1235
  */ const processUploadResult = (result, locationName, logContext, logger)=>{
1236
+ var _result_reason;
1130
1237
  if (result.status === 'fulfilled') {
1131
1238
  logger.info(`[DynamicWaasWalletClient] Successfully uploaded keyshares to ${locationName}`, logContext);
1132
1239
  return undefined;
1133
1240
  }
1134
1241
  const { message, stack } = getErrorDetails(result.reason);
1242
+ const isRetryable = ((_result_reason = result.reason) == null ? void 0 : _result_reason.isRetryable) !== false;
1135
1243
  logger.error(`[DynamicWaasWalletClient] Failed to upload keyshares to ${locationName}`, _extends({}, logContext, {
1136
1244
  error: message,
1137
1245
  errorStack: stack
1138
1246
  }));
1139
- return `Failed to backup keyshares to ${locationName}: ${message}`;
1247
+ return {
1248
+ locationName,
1249
+ message,
1250
+ isRetryable
1251
+ };
1140
1252
  };
1141
1253
  /**
1142
1254
  * Uploads a backup to Google Drive App
@@ -1176,16 +1288,27 @@ const initializeCloudKit = async (config, signInButtonId, onSignInRequired, onSi
1176
1288
  })
1177
1289
  ];
1178
1290
  const results = await Promise.allSettled(uploadPromises);
1179
- const errors = [
1291
+ const failures = [
1180
1292
  processUploadResult(results[0], 'Google Drive App Storage', logContext, logger),
1181
1293
  processUploadResult(results[1], 'Google Drive Personal', logContext, logger)
1182
- ].filter((error)=>error !== undefined);
1183
- if (errors.length > 0) {
1294
+ ].filter((failure)=>failure !== undefined);
1295
+ if (failures.length > 0) {
1184
1296
  logger.error('[DynamicWaasWalletClient] Google Drive backup failed', _extends({}, logContext, {
1185
- errorCount: errors.length,
1186
- errors
1297
+ errorCount: failures.length,
1298
+ errors: failures.map((f)=>`Failed to backup keyshares to ${f.locationName}: ${f.message}`)
1187
1299
  }));
1188
- throw new Error(`[DynamicWaasWalletClient] ${errors.join('; ')}`);
1300
+ // Both upload destinations (appDataFolder + personal) commonly fail with
1301
+ // the same underlying error (e.g., missing OAuth scope). When all failures
1302
+ // share one message, surface it once without per-location wrapping;
1303
+ // otherwise keep a short "Location: message" prefix per failure so the
1304
+ // user can see what differed.
1305
+ const uniqueMessages = [
1306
+ ...new Set(failures.map((f)=>f.message))
1307
+ ];
1308
+ const finalMessage = uniqueMessages.length === 1 ? uniqueMessages[0] : failures.map((f)=>`${f.locationName}: ${f.message}`).join(' | ');
1309
+ const aggregatedError = new Error(finalMessage);
1310
+ aggregatedError.isRetryable = failures.every((f)=>f.isRetryable);
1311
+ throw aggregatedError;
1189
1312
  }
1190
1313
  logger.info('[DynamicWaasWalletClient] Google Drive backup completed successfully', logContext);
1191
1314
  };
@@ -2103,18 +2226,33 @@ class DynamicWalletClient {
2103
2226
  }
2104
2227
  }
2105
2228
  async clientKeyGen({ chainName, roomId, serverKeygenIds, clientKeygenInitResults, thresholdSignatureScheme, bitcoinConfig, dynamicRequestId, traceContext }) {
2229
+ // Try forward MPC first if enabled
2106
2230
  if (this.forwardMPCEnabled) {
2107
- return this.forwardMPCClientKeygen({
2108
- chainName,
2109
- roomId,
2110
- serverKeygenIds,
2111
- clientKeygenInitResults,
2112
- thresholdSignatureScheme,
2113
- bitcoinConfig,
2114
- dynamicRequestId,
2115
- traceContext
2116
- });
2231
+ try {
2232
+ return await this.forwardMPCClientKeygen({
2233
+ chainName,
2234
+ roomId,
2235
+ serverKeygenIds,
2236
+ clientKeygenInitResults,
2237
+ thresholdSignatureScheme,
2238
+ bitcoinConfig,
2239
+ dynamicRequestId,
2240
+ traceContext
2241
+ });
2242
+ } catch (error) {
2243
+ const errorInfo = classifyForwardMpcError(error);
2244
+ this.logger.warn('Forward MPC keygen failed', _extends({}, errorInfo, {
2245
+ chainName,
2246
+ environmentId: this.environmentId,
2247
+ dynamicRequestId
2248
+ }));
2249
+ if (!errorInfo.shouldFallback) {
2250
+ throw error;
2251
+ }
2252
+ // Fall through to relay-based keygen below
2253
+ }
2117
2254
  }
2255
+ // Relay-based keygen (fallback or when forward MPC is disabled)
2118
2256
  // Get the chain config and the mpc signer
2119
2257
  const mpcSigner = getMPCSigner({
2120
2258
  chainName,
@@ -2432,20 +2570,35 @@ class DynamicWalletClient {
2432
2570
  derivationPath,
2433
2571
  isFormatted
2434
2572
  }, this.getTraceContext(traceContext)));
2573
+ // Try forward MPC first if enabled, with fallback to relay-based signing on attestation failure
2435
2574
  if (this.forwardMPCEnabled) {
2436
- return this.forwardMPCClientSign({
2437
- chainName,
2438
- message,
2439
- roomId,
2440
- keyShare,
2441
- derivationPath,
2442
- formattedMessage,
2443
- dynamicRequestId,
2444
- isFormatted,
2445
- traceContext,
2446
- bitcoinConfig
2447
- });
2575
+ try {
2576
+ return await this.forwardMPCClientSign({
2577
+ chainName,
2578
+ message,
2579
+ roomId,
2580
+ keyShare,
2581
+ derivationPath,
2582
+ formattedMessage,
2583
+ dynamicRequestId,
2584
+ isFormatted,
2585
+ traceContext,
2586
+ bitcoinConfig
2587
+ });
2588
+ } catch (error) {
2589
+ const errorInfo = classifyForwardMpcError(error);
2590
+ this.logger.warn('Forward MPC signing failed', _extends({}, errorInfo, {
2591
+ chainName,
2592
+ environmentId: this.environmentId,
2593
+ dynamicRequestId
2594
+ }));
2595
+ if (!errorInfo.shouldFallback) {
2596
+ throw error;
2597
+ }
2598
+ // Fall through to relay-based signing below
2599
+ }
2448
2600
  }
2601
+ // Relay-based signing (fallback or when forward MPC is disabled)
2449
2602
  // Unwrap MessageHash for BIP340 as it expects Uint8Array or hex string
2450
2603
  let messageToSign = formattedMessage;
2451
2604
  if (mpcSigner instanceof BIP340 && formattedMessage instanceof MessageHash) {
@@ -2692,6 +2845,52 @@ class DynamicWalletClient {
2692
2845
  }));
2693
2846
  }
2694
2847
  /**
2848
+ * Resolves backup locations for a reshare operation.
2849
+ *
2850
+ * When reshareOnNextSignOn triggers (e.g., after Google OAuth refresh), the caller
2851
+ * doesn't pass backup parameters. This method detects that case and preserves the
2852
+ * wallet's existing backup locations (Google Drive, iCloud, delegation) instead of
2853
+ * proceeding with empty backup locations.
2854
+ *
2855
+ * @returns The backup locations to use for the reshare operation
2856
+ */ resolveBackupLocationsForReshare({ oldThresholdSignatureScheme, newThresholdSignatureScheme, cloudProviders = [], delegateToProjectEnvironment = false, accountAddress, dynamicRequestId }) {
2857
+ const shouldPreserveExisting = shouldReshareToSameBackups({
2858
+ oldThresholdSignatureScheme,
2859
+ newThresholdSignatureScheme,
2860
+ cloudProviders,
2861
+ delegateToProjectEnvironment
2862
+ });
2863
+ if (!shouldPreserveExisting) {
2864
+ return {
2865
+ cloudProviders,
2866
+ delegateToProjectEnvironment
2867
+ };
2868
+ }
2869
+ // Wallet is guaranteed to be in map - verifyPassword calls requireWalletFromMap upstream
2870
+ const walletProperties = this.getWalletFromMap(accountAddress);
2871
+ if (!walletProperties) {
2872
+ this.logger.warn('[resolveBackupLocationsForReshare] Wallet not in map, using passed params', {
2873
+ dynamicRequestId,
2874
+ accountAddress
2875
+ });
2876
+ return {
2877
+ cloudProviders,
2878
+ delegateToProjectEnvironment
2879
+ };
2880
+ }
2881
+ const preservedCloudProviders = getActiveCloudProviders(walletProperties.clientKeySharesBackupInfo);
2882
+ const preservedDelegation = hasDelegatedBackup(walletProperties.clientKeySharesBackupInfo);
2883
+ this.logger.info('[resolveBackupLocationsForReshare] Preserving existing backup locations', {
2884
+ dynamicRequestId,
2885
+ cloudProviders: preservedCloudProviders,
2886
+ delegateToProjectEnvironment: preservedDelegation
2887
+ });
2888
+ return {
2889
+ cloudProviders: preservedCloudProviders,
2890
+ delegateToProjectEnvironment: preservedDelegation
2891
+ };
2892
+ }
2893
+ /**
2695
2894
  * Gets the Bitcoin config for MPC operations by looking up addressType
2696
2895
  * from walletMap, with fallback to deriving it from derivationPath.
2697
2896
  */ getBitcoinConfigForChain(chainName, accountAddress) {
@@ -2908,6 +3107,8 @@ class DynamicWalletClient {
2908
3107
  }
2909
3108
  async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false }) {
2910
3109
  const dynamicRequestId = v4();
3110
+ // Password validation - wrapped in try-catch for consistent error handling
3111
+ // This path should NOT wipe key shares on failure (shares remain valid)
2911
3112
  try {
2912
3113
  this.assertPasswordRequired(password);
2913
3114
  await this.verifyPassword({
@@ -2920,6 +3121,30 @@ class DynamicWalletClient {
2920
3121
  password,
2921
3122
  signedSessionId
2922
3123
  });
3124
+ } catch (error) {
3125
+ // Log password validation error but do NOT wipe key shares
3126
+ logError({
3127
+ message: '[DynamicWaasWalletClient]: Password validation failed during reshare',
3128
+ error: error,
3129
+ context: {
3130
+ accountAddress,
3131
+ dynamicRequestId
3132
+ }
3133
+ });
3134
+ throw error;
3135
+ }
3136
+ // Resolve backup locations for this reshare - may preserve existing locations
3137
+ // when reshareOnNextSignOn triggers without explicit backup parameters
3138
+ const { cloudProviders: resolvedCloudProviders, delegateToProjectEnvironment: resolvedDelegation } = this.resolveBackupLocationsForReshare({
3139
+ oldThresholdSignatureScheme,
3140
+ newThresholdSignatureScheme,
3141
+ cloudProviders,
3142
+ delegateToProjectEnvironment,
3143
+ accountAddress,
3144
+ dynamicRequestId
3145
+ });
3146
+ // MPC ceremony path - errors here SHOULD wipe shares as they may be inconsistent
3147
+ try {
2923
3148
  const { existingClientShareCount } = getReshareConfig({
2924
3149
  oldThresholdSignatureScheme,
2925
3150
  newThresholdSignatureScheme
@@ -2954,7 +3179,7 @@ class DynamicWalletClient {
2954
3179
  oldThresholdSignatureScheme,
2955
3180
  newThresholdSignatureScheme,
2956
3181
  dynamicRequestId,
2957
- delegateToProjectEnvironment,
3182
+ delegateToProjectEnvironment: resolvedDelegation,
2958
3183
  mfaToken,
2959
3184
  elevatedAccessToken,
2960
3185
  revokeDelegation
@@ -3047,6 +3272,7 @@ class DynamicWalletClient {
3047
3272
  dynamicRequestId
3048
3273
  });
3049
3274
  // Retry the entire reshare operation (need new room and keygen IDs from server)
3275
+ // Pass effective* values so retry uses same backup locations
3050
3276
  return this.internalReshare({
3051
3277
  chainName,
3052
3278
  accountAddress,
@@ -3054,8 +3280,8 @@ class DynamicWalletClient {
3054
3280
  newThresholdSignatureScheme,
3055
3281
  password,
3056
3282
  signedSessionId,
3057
- cloudProviders,
3058
- delegateToProjectEnvironment,
3283
+ cloudProviders: resolvedCloudProviders,
3284
+ delegateToProjectEnvironment: resolvedDelegation,
3059
3285
  mfaToken,
3060
3286
  elevatedAccessToken,
3061
3287
  revokeDelegation,
@@ -3070,26 +3296,27 @@ class DynamicWalletClient {
3070
3296
  ];
3071
3297
  let distribution;
3072
3298
  // Generic distribution logic - works with any cloud providers
3073
- if (delegateToProjectEnvironment && cloudProviders.length > 0) {
3299
+ // Use effective* variables which may have been populated from existing wallet state
3300
+ if (resolvedDelegation && resolvedCloudProviders.length > 0) {
3074
3301
  // Delegation + Cloud Providers: Client's existing share backs up to both Dynamic and cloud providers.
3075
3302
  // The new share goes to the webhook for delegation.
3076
3303
  distribution = createDelegationWithCloudProviderDistribution({
3077
- providers: cloudProviders,
3304
+ providers: resolvedCloudProviders,
3078
3305
  existingShares: existingReshareResults,
3079
3306
  delegatedShare: newReshareResults[0]
3080
3307
  });
3081
- } else if (delegateToProjectEnvironment) {
3308
+ } else if (resolvedDelegation) {
3082
3309
  // Delegation only: Client's existing share backs up to Dynamic.
3083
3310
  // The new share goes to the webhook for delegation. No cloud provider backup.
3084
3311
  distribution = createDelegationOnlyDistribution({
3085
3312
  existingShares: existingReshareResults,
3086
3313
  delegatedShare: newReshareResults[0]
3087
3314
  });
3088
- } else if (cloudProviders.length > 0) {
3315
+ } else if (resolvedCloudProviders.length > 0) {
3089
3316
  // Cloud Providers only: Split shares between Dynamic (N-1) and cloud providers (1).
3090
3317
  // The last share (new share) goes to cloud providers.
3091
3318
  distribution = createCloudProviderDistribution({
3092
- providers: cloudProviders,
3319
+ providers: resolvedCloudProviders,
3093
3320
  allShares: allClientShares
3094
3321
  });
3095
3322
  } else {
@@ -3127,6 +3354,8 @@ class DynamicWalletClient {
3127
3354
  oldThresholdSignatureScheme,
3128
3355
  newThresholdSignatureScheme,
3129
3356
  cloudProviders,
3357
+ resolvedCloudProviders,
3358
+ resolvedDelegation,
3130
3359
  dynamicRequestId
3131
3360
  }
3132
3361
  });
@@ -4170,12 +4399,14 @@ class DynamicWalletClient {
4170
4399
  const oauthAccountId = getGoogleOAuthAccountId(user == null ? void 0 : user.verifiedCredentials);
4171
4400
  if (!oauthAccountId) {
4172
4401
  const error = new Error('No Google OAuth account ID found');
4402
+ // PII: intentionally omitting full user object
4173
4403
  logError({
4174
4404
  message: 'No Google OAuth account ID found',
4175
4405
  error,
4176
4406
  context: {
4177
- user,
4178
- accountAddress
4407
+ accountAddress,
4408
+ userId: user == null ? void 0 : user.id,
4409
+ projectEnvironmentId: user == null ? void 0 : user.projectEnvironmentId
4179
4410
  }
4180
4411
  });
4181
4412
  throw error;
@@ -5439,4 +5670,4 @@ DynamicWalletClient.rooms = {};
5439
5670
  DynamicWalletClient.roomsInitializing = {};
5440
5671
  DynamicWalletClient.roomsPersistChain = Promise.resolve();
5441
5672
 
5442
- export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_KEYGEN_FAILED, ERROR_PASSCODE_REQUIRED, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getGoogleOAuthAccountId, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, listICloudBackups, mergeUniqueKeyShares, readEnvironmentSettings, retryPromise, timeoutPromise };
5673
+ export { BACKUP_FILENAME, CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX, DynamicWalletClient, ENVIRONMENT_SETTINGS_STORAGE_KEY, ERROR_ACCOUNT_ADDRESS_REQUIRED, ERROR_CREATE_WALLET_ACCOUNT, ERROR_EXPORT_PRIVATE_KEY, ERROR_IMPORT_PRIVATE_KEY, ERROR_KEYGEN_FAILED, ERROR_PASSCODE_REQUIRED, ERROR_PASSWORD_MISMATCH, ERROR_PASSWORD_REQUIRED_FOR_ENCRYPTED_WALLET, ERROR_PUBLIC_KEY_MISMATCH, ERROR_REFRESH_NOT_SUPPORTED_FOR_TWO_OF_THREE, ERROR_SIGN_MESSAGE, ERROR_SIGN_TYPED_DATA, ERROR_VERIFY_MESSAGE_SIGNATURE, ERROR_VERIFY_TRANSACTION_SIGNATURE, HEAVY_QUEUE_OPERATIONS, RECOVER_QUEUE_OPERATIONS, ROOM_CACHE_COUNT, ROOM_EXPIRATION_TIME, SIGNED_SESSION_ID_MIN_VERSION, SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE, SIGN_QUEUE_OPERATIONS, STORAGE_KEY, WalletBusyError, WalletNotReadyError, cancelICloudAuth, createAddCloudProviderToExistingDelegationDistribution, createBackupData, createCloudProviderDistribution, createDelegationOnlyDistribution, createDelegationWithCloudProviderDistribution, createDynamicOnlyDistribution, deleteICloudBackup, downloadStringAsFile, extractPubkey, formatEvmMessage, formatMessage, getActiveCloudProviders, getBitcoinAddressTypeFromDerivationPath, getClientKeyShareBackupInfo, getClientKeyShareExportFileName, getGoogleOAuthAccountId, getICloudBackup, getMPCSignatureScheme, getMPCSigner, hasCloudProviderBackup, hasDelegatedBackup, hasEncryptedSharesCached, initializeCloudKit, isBrowser, isHeavyQueueOperation, isHexString, isICloudAuthenticated, isPublicKeyMismatchError, isRecoverQueueOperation, isSignQueueOperation, listICloudBackups, mergeUniqueKeyShares, readEnvironmentSettings, retryPromise, shouldReshareToSameBackups, timeoutPromise };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@dynamic-labs-wallet/browser",
3
- "version": "0.0.325",
3
+ "version": "0.0.327",
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": "0.0.325",
7
+ "@dynamic-labs-wallet/core": "0.0.327",
8
+ "@dynamic-labs-wallet/forward-mpc-client": "0.7.0",
8
9
  "@dynamic-labs/sdk-api-core": "^0.0.900",
9
10
  "argon2id": "1.0.1",
10
11
  "axios": "1.15.0",
@@ -29,7 +30,7 @@
29
30
  "build": {}
30
31
  }
31
32
  },
32
- "main": "./index.cjs.js",
33
+ "main": "./index.cjs",
33
34
  "module": "./index.esm.js",
34
35
  "types": "./index.esm.d.ts",
35
36
  "exports": {
@@ -37,7 +38,7 @@
37
38
  ".": {
38
39
  "types": "./index.esm.d.ts",
39
40
  "import": "./index.esm.js",
40
- "require": "./index.cjs.js",
41
+ "require": "./index.cjs",
41
42
  "default": "./index.esm.js"
42
43
  }
43
44
  },
@@ -1,3 +1,27 @@
1
+ type GoogleDriveErrorBody = {
2
+ error?: {
3
+ code?: number;
4
+ status?: string;
5
+ message?: string;
6
+ errors?: Array<{
7
+ reason?: string;
8
+ message?: string;
9
+ }>;
10
+ details?: Array<{
11
+ reason?: string;
12
+ '@type'?: string;
13
+ }>;
14
+ };
15
+ };
16
+ type MappedUploadError = {
17
+ message: string;
18
+ isRetryable: boolean;
19
+ };
20
+ /**
21
+ * Maps a Google Drive API error to an actionable user-facing message and a retry hint.
22
+ * The retry hint is consumed by retryPromise to short-circuit on non-transient failures.
23
+ */
24
+ export declare const mapGoogleDriveUploadError: (httpStatus: number, body: GoogleDriveErrorBody | null) => MappedUploadError;
1
25
  export declare const uploadFileToGoogleDriveAppStorage: ({ accessToken, fileName, jsonData, }: {
2
26
  accessToken: string;
3
27
  fileName: string;
@@ -16,4 +40,5 @@ export declare const downloadFileFromGoogleDrive: ({ accessToken, fileName, }: {
16
40
  accessToken: string;
17
41
  fileName: string;
18
42
  }) => Promise<unknown | null>;
43
+ export {};
19
44
  //# sourceMappingURL=googleDrive.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"googleDrive.d.ts","sourceRoot":"","sources":["../../../src/backup/providers/googleDrive.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,iCAAiC,yCAI3C;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,iBAOA,CAAC;AAEF,eAAO,MAAM,+BAA+B,yCAIzC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,iBAOA,CAAC;AAmEF,eAAO,MAAM,wBAAwB,+BAGlC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,iBAsBA,CAAC;AAEF,eAAO,MAAM,2BAA2B,+BAGrC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CA6BzB,CAAC"}
1
+ {"version":3,"file":"googleDrive.d.ts","sourceRoot":"","sources":["../../../src/backup/providers/googleDrive.ts"],"names":[],"mappings":"AAWA,KAAK,oBAAoB,GAAG;IAC1B,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QACtD,OAAO,CAAC,EAAE,KAAK,CAAC;YAAE,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACxD,CAAC;CACH,CAAC;AAEF,KAAK,iBAAiB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC;AAiBnE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,eAAgB,MAAM,QAAQ,oBAAoB,GAAG,IAAI,KAAG,iBA6CjG,CAAC;AAgBF,eAAO,MAAM,iCAAiC,yCAI3C;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,iBAOA,CAAC;AAEF,eAAO,MAAM,+BAA+B,yCAIzC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB,iBAOA,CAAC;AA0FF,eAAO,MAAM,wBAAwB,+BAGlC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,iBAuBA,CAAC;AAEF,eAAO,MAAM,2BAA2B,+BAGrC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB,KAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CA6BzB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/backup/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAuCzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,uGASnC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,GAAG,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,kBAiDA,CAAC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/backup/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AA8CzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,yBAAyB,uGASnC;IACD,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,GAAG,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;CACjB,kBA+DA,CAAC"}
package/src/client.d.ts CHANGED
@@ -276,6 +276,17 @@ export declare class DynamicWalletClient {
276
276
  elevatedAccessToken?: string;
277
277
  traceContext?: TraceContext;
278
278
  }): Promise<void>;
279
+ /**
280
+ * Resolves backup locations for a reshare operation.
281
+ *
282
+ * When reshareOnNextSignOn triggers (e.g., after Google OAuth refresh), the caller
283
+ * doesn't pass backup parameters. This method detects that case and preserves the
284
+ * wallet's existing backup locations (Google Drive, iCloud, delegation) instead of
285
+ * proceeding with empty backup locations.
286
+ *
287
+ * @returns The backup locations to use for the reshare operation
288
+ */
289
+ private resolveBackupLocationsForReshare;
279
290
  /**
280
291
  * Gets the Bitcoin config for MPC operations by looking up addressType
281
292
  * from walletMap, with fallback to deriving it from derivationPath.