@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.cjs.js → index.cjs} +285 -53
- package/index.esm.js +286 -55
- package/package.json +5 -4
- package/src/backup/providers/googleDrive.d.ts +25 -0
- package/src/backup/providers/googleDrive.d.ts.map +1 -1
- package/src/backup/utils.d.ts.map +1 -1
- package/src/client.d.ts +11 -0
- package/src/client.d.ts.map +1 -1
- package/src/utils.d.ts +24 -1
- package/src/utils.d.ts.map +1 -1
- /package/{index.cjs.d.ts → index.d.ts} +0 -0
|
@@ -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
|
|
377
|
-
|
|
378
|
-
|
|
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:
|
|
457
|
+
throw new Error(`Upload verification failed: could not list files in ${spacesParam}`);
|
|
385
458
|
}
|
|
386
459
|
const verifyResult = await verifyResponse.json();
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
431
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
1182
|
-
if (
|
|
1293
|
+
].filter((failure)=>failure !== undefined);
|
|
1294
|
+
if (failures.length > 0) {
|
|
1183
1295
|
logger.error('[DynamicWaasWalletClient] Google Drive backup failed', _extends({}, logContext, {
|
|
1184
|
-
errorCount:
|
|
1185
|
-
errors
|
|
1296
|
+
errorCount: failures.length,
|
|
1297
|
+
errors: failures.map((f)=>`Failed to backup keyshares to ${f.locationName}: ${f.message}`)
|
|
1186
1298
|
}));
|
|
1187
|
-
|
|
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
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
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
|
-
|
|
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:
|
|
3303
|
+
providers: resolvedCloudProviders,
|
|
3077
3304
|
existingShares: existingReshareResults,
|
|
3078
3305
|
delegatedShare: newReshareResults[0]
|
|
3079
3306
|
});
|
|
3080
|
-
} else if (
|
|
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 (
|
|
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:
|
|
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
|
-
|
|
4177
|
-
|
|
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, {
|