@dynamic-labs-wallet/browser 1.0.98 → 1.0.99
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 +354 -40
- package/index.esm.js +352 -42
- package/package.json +3 -3
- package/src/backup/providers/googleDrive.d.ts +1 -1
- package/src/backup/providers/googleDrive.d.ts.map +1 -1
- package/src/client.d.ts +35 -3
- package/src/client.d.ts.map +1 -1
- package/src/offlineRecovery.d.ts +66 -0
- package/src/offlineRecovery.d.ts.map +1 -0
- package/src/selectReshareDistribution.d.ts +3 -2
- package/src/selectReshareDistribution.d.ts.map +1 -1
- package/src/types.d.ts +39 -0
- package/src/types.d.ts.map +1 -1
- package/src/utils.d.ts +19 -2
- package/src/utils.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -1274,6 +1274,19 @@ const downloadFileFromGoogleDrive = async ({ accessToken, fileName })=>{
|
|
|
1274
1274
|
}
|
|
1275
1275
|
};
|
|
1276
1276
|
|
|
1277
|
+
function _object_without_properties_loose(source, excluded) {
|
|
1278
|
+
if (source == null) return {};
|
|
1279
|
+
var target = {};
|
|
1280
|
+
var sourceKeys = Object.keys(source);
|
|
1281
|
+
var key, i;
|
|
1282
|
+
for(i = 0; i < sourceKeys.length; i++){
|
|
1283
|
+
key = sourceKeys[i];
|
|
1284
|
+
if (excluded.indexOf(key) >= 0) continue;
|
|
1285
|
+
target[key] = source[key];
|
|
1286
|
+
}
|
|
1287
|
+
return target;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1277
1290
|
/**
|
|
1278
1291
|
* Normalizes an address to lowercase for consistent map key lookups.
|
|
1279
1292
|
* This ensures that addresses with different casing (e.g., EIP-55 checksummed vs lowercase)
|
|
@@ -1301,9 +1314,17 @@ const isBrowser = ()=>globalThis.window !== undefined;
|
|
|
1301
1314
|
}
|
|
1302
1315
|
return actualPubkey;
|
|
1303
1316
|
};
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1317
|
+
/**
|
|
1318
|
+
* Keeps a variant's Drive file from colliding with — and so shadowing — the
|
|
1319
|
+
* wallet's own (rootUser) backup, which can carry the same
|
|
1320
|
+
* `thresholdSignatureScheme` and address.
|
|
1321
|
+
*/ const getExportFileNameSuffix = ({ isGoogleDrive = false, isOfflineRecovery = false })=>`${isGoogleDrive ? '-google-drive' : ''}${isOfflineRecovery ? '-offline-recovery' : ''}`;
|
|
1322
|
+
const getClientKeyShareExportFileName = (_param)=>{
|
|
1323
|
+
var { thresholdSignatureScheme, accountAddress } = _param, variant = _object_without_properties_loose(_param, [
|
|
1324
|
+
"thresholdSignatureScheme",
|
|
1325
|
+
"accountAddress"
|
|
1326
|
+
]);
|
|
1327
|
+
return `${CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX}-${thresholdSignatureScheme}-${accountAddress}${getExportFileNameSuffix(variant)}.json`;
|
|
1307
1328
|
};
|
|
1308
1329
|
const getClientKeyShareBackupInfo = (params)=>{
|
|
1309
1330
|
var _params_walletProperties, _params_walletProperties_keyShares_;
|
|
@@ -1338,6 +1359,25 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
1338
1359
|
passwordEncrypted
|
|
1339
1360
|
};
|
|
1340
1361
|
};
|
|
1362
|
+
/**
|
|
1363
|
+
* Records the just-activated offlineRecovery share set against the wallet's
|
|
1364
|
+
* own (rootUser) backup info, which it must leave otherwise untouched — the
|
|
1365
|
+
* exported set is a separate share set, not rootUser's.
|
|
1366
|
+
*
|
|
1367
|
+
* Only the `offlineRecovery` location is taken from `activated`. The exported
|
|
1368
|
+
* share is deliberately also reported under `googleDrive` so the server can
|
|
1369
|
+
* activate it, but folding that tag in here would make the wallet look like
|
|
1370
|
+
* rootUser itself is backed up to Drive — see `getActiveCloudProviders`, whose
|
|
1371
|
+
* result drives later reshare/delegation decisions. The location is replaced
|
|
1372
|
+
* rather than appended so repeated offline recoveries stay idempotent.
|
|
1373
|
+
*/ const recordOfflineRecoveryBackupLocations = (existing, activated)=>{
|
|
1374
|
+
var _activated_backups_BackupLocation_OFFLINE_RECOVERY;
|
|
1375
|
+
return _extends({}, existing, {
|
|
1376
|
+
backups: _extends({}, existing.backups, {
|
|
1377
|
+
[core.BackupLocation.OFFLINE_RECOVERY]: (_activated_backups_BackupLocation_OFFLINE_RECOVERY = activated.backups[core.BackupLocation.OFFLINE_RECOVERY]) != null ? _activated_backups_BackupLocation_OFFLINE_RECOVERY : []
|
|
1378
|
+
})
|
|
1379
|
+
});
|
|
1380
|
+
};
|
|
1341
1381
|
const timeoutPromise = ({ timeInMs, activity = 'Ceremony' })=>{
|
|
1342
1382
|
return new Promise((_, reject)=>setTimeout(()=>reject(new Error(`${activity} did not complete in ${timeInMs}ms`)), timeInMs));
|
|
1343
1383
|
};
|
|
@@ -2472,6 +2512,32 @@ class WalletBusyError extends Error {
|
|
|
2472
2512
|
this.accountAddress = accountAddress;
|
|
2473
2513
|
}
|
|
2474
2514
|
}
|
|
2515
|
+
/**
|
|
2516
|
+
* Thrown when a Google Drive operation fails because the user's OAuth access
|
|
2517
|
+
* token has expired or is otherwise unusable. Distinct from a generic error so
|
|
2518
|
+
* the caller can prompt the user to re-link Google before retrying.
|
|
2519
|
+
*/ class ExpiredGoogleDriveTokenError extends Error {
|
|
2520
|
+
constructor(message, options){
|
|
2521
|
+
super(message, (options == null ? void 0 : options.cause) !== undefined ? {
|
|
2522
|
+
cause: options.cause
|
|
2523
|
+
} : undefined);
|
|
2524
|
+
this.name = 'ExpiredGoogleDriveTokenError';
|
|
2525
|
+
this.accountAddress = options == null ? void 0 : options.accountAddress;
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* Thrown when a Google Drive backup preflight blocks for a reason OTHER than
|
|
2530
|
+
* an expired/scope-insufficient token (e.g. storage quota, rate limit,
|
|
2531
|
+
* network). Distinct from {@link ExpiredGoogleDriveTokenError} so callers
|
|
2532
|
+
* don't prompt a Google re-link for a problem re-linking can't fix.
|
|
2533
|
+
*/ class GoogleDriveBackupBlockedError extends Error {
|
|
2534
|
+
constructor(message, errorReason, options){
|
|
2535
|
+
super(message);
|
|
2536
|
+
this.name = 'GoogleDriveBackupBlockedError';
|
|
2537
|
+
this.errorReason = errorReason;
|
|
2538
|
+
this.accountAddress = options == null ? void 0 : options.accountAddress;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2475
2541
|
/**
|
|
2476
2542
|
* Creates distribution where shares go to specified cloud providers
|
|
2477
2543
|
* Last share goes to cloud providers, rest to Dynamic backend
|
|
@@ -2554,6 +2620,22 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
2554
2620
|
clientShares: allShares,
|
|
2555
2621
|
cloudProviderShares: {}
|
|
2556
2622
|
});
|
|
2623
|
+
/**
|
|
2624
|
+
* Offline recovery's exported set is minted via the same same-parties 2/2
|
|
2625
|
+
* reshare as delegation. The client only ever sees its own reshared share —
|
|
2626
|
+
* upload it to Google Drive (mandatory, no device-export variant). The
|
|
2627
|
+
* server's own reshared share is delivered to the developer's webhook
|
|
2628
|
+
* entirely server-side (see redcoast); the browser SDK never touches it.
|
|
2629
|
+
* clientShares: [] skips the local-storage rewrite — root_user is untouched.
|
|
2630
|
+
*/ const createOfflineRecoveryDistribution = ({ exportedClientShare })=>({
|
|
2631
|
+
clientShares: [],
|
|
2632
|
+
cloudProviderShares: {
|
|
2633
|
+
[core.BackupLocation.GOOGLE_DRIVE]: [
|
|
2634
|
+
exportedClientShare
|
|
2635
|
+
]
|
|
2636
|
+
},
|
|
2637
|
+
isOfflineRecovery: true
|
|
2638
|
+
});
|
|
2557
2639
|
/**
|
|
2558
2640
|
* Returns the share set of the given `shareSetType` from a wallet's
|
|
2559
2641
|
* `otherShareSets` array, if one exists.
|
|
@@ -2863,7 +2945,10 @@ const collectDynamicKeyShareIds = (locations)=>{
|
|
|
2863
2945
|
// ShareDistribution that storage/publish should follow. Lives outside the
|
|
2864
2946
|
// client class so it's straightforward to reason about and unit-test
|
|
2865
2947
|
// without spinning up DynamicWalletClient.
|
|
2866
|
-
const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders, existingReshareResults, newReshareResults })=>{
|
|
2948
|
+
const selectReshareDistribution = ({ resolvedDelegation, resolvedOfflineRecovery = false, resolvedCloudProviders, existingReshareResults, newReshareResults })=>{
|
|
2949
|
+
if (resolvedDelegation && resolvedOfflineRecovery) {
|
|
2950
|
+
throw new Error('selectReshareDistribution: resolvedDelegation and resolvedOfflineRecovery are mutually exclusive on a single reshare');
|
|
2951
|
+
}
|
|
2867
2952
|
const allClientShares = [
|
|
2868
2953
|
...existingReshareResults,
|
|
2869
2954
|
...newReshareResults
|
|
@@ -2892,6 +2977,19 @@ const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders,
|
|
|
2892
2977
|
delegatedShare: newReshareResults[0]
|
|
2893
2978
|
});
|
|
2894
2979
|
}
|
|
2980
|
+
// Offline recovery mints its exported share set via the same same-parties
|
|
2981
|
+
// reshare signal as delegation (1 existing, 0 new). The client's reshared
|
|
2982
|
+
// share routes to Google Drive (mandatory — no device-export variant); the
|
|
2983
|
+
// server's own reshared share is delivered to the developer entirely
|
|
2984
|
+
// server-side (redcoast) and never appears here.
|
|
2985
|
+
if (resolvedOfflineRecovery && newReshareResults.length === 0 && existingReshareResults.length === 1) {
|
|
2986
|
+
if (!resolvedCloudProviders.includes(core.BackupLocation.GOOGLE_DRIVE)) {
|
|
2987
|
+
throw new Error('selectReshareDistribution: offline recovery requires Google Drive');
|
|
2988
|
+
}
|
|
2989
|
+
return createOfflineRecoveryDistribution({
|
|
2990
|
+
exportedClientShare: existingReshareResults[0]
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2895
2993
|
if (resolvedCloudProviders.length > 0) {
|
|
2896
2994
|
return createCloudProviderDistribution({
|
|
2897
2995
|
providers: resolvedCloudProviders,
|
|
@@ -3059,6 +3157,123 @@ const readEnvironmentSettings = ()=>{
|
|
|
3059
3157
|
}
|
|
3060
3158
|
});
|
|
3061
3159
|
|
|
3160
|
+
const DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER = core.BackupLocation.GOOGLE_DRIVE;
|
|
3161
|
+
/**
|
|
3162
|
+
* Resolve a usable Google Drive access token. Unlike an opportunistic
|
|
3163
|
+
* multi-provider reshare (which silently drops Drive on failure), Drive is
|
|
3164
|
+
* mandatory here — resolution/preflight failures throw before the ceremony
|
|
3165
|
+
* starts. Only an actual token/scope problem throws `ExpiredGoogleDriveTokenError`;
|
|
3166
|
+
* quota, rate-limit, and network blocks throw `GoogleDriveBackupBlockedError`
|
|
3167
|
+
* instead, since re-linking Google fixes none of those.
|
|
3168
|
+
*/ const resolveGoogleDriveAccessToken = async (client, accountAddress, googleDriveAccessToken)=>{
|
|
3169
|
+
let token;
|
|
3170
|
+
try {
|
|
3171
|
+
token = googleDriveAccessToken != null ? googleDriveAccessToken : await client.fetchGoogleDriveAccessToken(accountAddress);
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
throw new ExpiredGoogleDriveTokenError('Offline recovery requires a valid Google Drive access token', {
|
|
3174
|
+
cause: error,
|
|
3175
|
+
accountAddress
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
const { blocking } = await runGoogleDriveBackupPreflight(token);
|
|
3179
|
+
if (!blocking) return token;
|
|
3180
|
+
if (blocking.errorReason === 'auth_denied') {
|
|
3181
|
+
throw new ExpiredGoogleDriveTokenError('Offline recovery requires a valid Google Drive access token', {
|
|
3182
|
+
accountAddress
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
throw new GoogleDriveBackupBlockedError(blocking.message, blocking.errorReason, {
|
|
3186
|
+
accountAddress
|
|
3187
|
+
});
|
|
3188
|
+
};
|
|
3189
|
+
const googleDriveStrategy = {
|
|
3190
|
+
location: core.BackupLocation.GOOGLE_DRIVE,
|
|
3191
|
+
prepareReshareArgs: async (client, params)=>({
|
|
3192
|
+
googleDriveAccessToken: await resolveGoogleDriveAccessToken(client, params.accountAddress, params.cloudProviderAccessToken)
|
|
3193
|
+
})
|
|
3194
|
+
};
|
|
3195
|
+
// Registry of supported providers. iCloud and other third-party clouds slot in
|
|
3196
|
+
// here (each supplying its own readiness check in `prepareReshareArgs`) without
|
|
3197
|
+
// changing `performOfflineRecoveryOperation`. Requesting an unregistered
|
|
3198
|
+
// provider throws below.
|
|
3199
|
+
const CLOUD_PROVIDER_STRATEGIES = {
|
|
3200
|
+
[core.BackupLocation.GOOGLE_DRIVE]: googleDriveStrategy
|
|
3201
|
+
};
|
|
3202
|
+
const getCloudProviderStrategy = (provider)=>{
|
|
3203
|
+
const strategy = CLOUD_PROVIDER_STRATEGIES[provider];
|
|
3204
|
+
if (!strategy) {
|
|
3205
|
+
throw new Error(`Offline recovery is not supported for cloud provider "${provider}"`);
|
|
3206
|
+
}
|
|
3207
|
+
return strategy;
|
|
3208
|
+
};
|
|
3209
|
+
// Generous upper bound on an OAuth bearer token — not chain-address validation
|
|
3210
|
+
// (accountAddress spans EVM/BTC/SVM/TON/SUI formats, so no shared regex fits).
|
|
3211
|
+
const MAX_CLOUD_PROVIDER_ACCESS_TOKEN_LENGTH = 4096;
|
|
3212
|
+
const validateOfflineRecoveryParams = ({ accountAddress, cloudProviderAccessToken })=>{
|
|
3213
|
+
if (typeof accountAddress !== 'string' || accountAddress.length === 0) {
|
|
3214
|
+
throw new Error('offlineRecovery requires a non-empty accountAddress');
|
|
3215
|
+
}
|
|
3216
|
+
if (cloudProviderAccessToken !== undefined && (typeof cloudProviderAccessToken !== 'string' || cloudProviderAccessToken.length === 0 || cloudProviderAccessToken.length > MAX_CLOUD_PROVIDER_ACCESS_TOKEN_LENGTH)) {
|
|
3217
|
+
throw new Error('offlineRecovery: cloudProviderAccessToken must be a non-empty string');
|
|
3218
|
+
}
|
|
3219
|
+
};
|
|
3220
|
+
/**
|
|
3221
|
+
* Mints the dedicated 2/2 `offlineRecovery` share set: the same
|
|
3222
|
+
* same-parties reshare mechanism delegation uses, routed to the chosen cloud
|
|
3223
|
+
* provider instead of the delegation webhook (see `selectReshareDistribution`).
|
|
3224
|
+
* The server's own reshared share is delivered to the developer entirely
|
|
3225
|
+
* server-side — this flow never touches it.
|
|
3226
|
+
*/ const performOfflineRecoveryOperation = async (client, params)=>{
|
|
3227
|
+
validateOfflineRecoveryParams(params);
|
|
3228
|
+
const { accountAddress, password, signedSessionId, sessionPublicKey, mfaToken } = params;
|
|
3229
|
+
var _params_cloudProvider;
|
|
3230
|
+
const provider = (_params_cloudProvider = params.cloudProvider) != null ? _params_cloudProvider : DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER;
|
|
3231
|
+
try {
|
|
3232
|
+
const wallet = await client.getWallet({
|
|
3233
|
+
accountAddress,
|
|
3234
|
+
walletOperation: core.WalletOperation.REACH_ALL_PARTIES,
|
|
3235
|
+
password,
|
|
3236
|
+
signedSessionId
|
|
3237
|
+
});
|
|
3238
|
+
if (wallet.chainName === 'SUI') {
|
|
3239
|
+
throw new Error('Offline recovery is not allowed for SUI');
|
|
3240
|
+
}
|
|
3241
|
+
const strategy = getCloudProviderStrategy(provider);
|
|
3242
|
+
const providerReshareArgs = await strategy.prepareReshareArgs(client, params);
|
|
3243
|
+
const walletData = client.getWalletFromMap(accountAddress);
|
|
3244
|
+
if (!walletData) {
|
|
3245
|
+
throw new Error(`Wallet not found for address: ${accountAddress}`);
|
|
3246
|
+
}
|
|
3247
|
+
await client.reshare(_extends({
|
|
3248
|
+
chainName: walletData.chainName,
|
|
3249
|
+
accountAddress,
|
|
3250
|
+
oldThresholdSignatureScheme: walletData.thresholdSignatureScheme,
|
|
3251
|
+
// Pinning 2/2 (the exported set's real shape) is what makes getReshareConfig
|
|
3252
|
+
// yield 1 existing / 0 new — the same-parties shape selectReshareDistribution's
|
|
3253
|
+
// offline-recovery branch requires. The wallet's own scheme would break
|
|
3254
|
+
// 2-of-3 wallets: 1/1, missing the branch, falling through to a plain reshare.
|
|
3255
|
+
newThresholdSignatureScheme: core.ThresholdSignatureScheme.TWO_OF_TWO,
|
|
3256
|
+
password,
|
|
3257
|
+
signedSessionId,
|
|
3258
|
+
sessionPublicKey,
|
|
3259
|
+
cloudProviders: [
|
|
3260
|
+
strategy.location
|
|
3261
|
+
],
|
|
3262
|
+
offlineRecovery: true,
|
|
3263
|
+
mfaToken
|
|
3264
|
+
}, providerReshareArgs));
|
|
3265
|
+
} catch (error) {
|
|
3266
|
+
logError({
|
|
3267
|
+
message: 'Error in offlineRecovery',
|
|
3268
|
+
error: error,
|
|
3269
|
+
context: {
|
|
3270
|
+
accountAddress
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3273
|
+
throw error;
|
|
3274
|
+
}
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3062
3277
|
/**
|
|
3063
3278
|
* Determines the recovery state of a wallet based on backup info and local shares.
|
|
3064
3279
|
*
|
|
@@ -3191,7 +3406,9 @@ class DynamicWalletClient {
|
|
|
3191
3406
|
/**
|
|
3192
3407
|
* Get wallet properties from the wallet map using normalized address.
|
|
3193
3408
|
* Normalizes the address to lowercase for consistent lookups regardless of input casing.
|
|
3194
|
-
*/
|
|
3409
|
+
*/ // Public (was protected) so the extracted offlineRecovery flow can read
|
|
3410
|
+
// wallet state off the client instance — see ./offlineRecovery.ts.
|
|
3411
|
+
getWalletFromMap(accountAddress) {
|
|
3195
3412
|
return this.walletMap[normalizeAddress(accountAddress)];
|
|
3196
3413
|
}
|
|
3197
3414
|
/**
|
|
@@ -5102,7 +5319,10 @@ class DynamicWalletClient {
|
|
|
5102
5319
|
existingClientKeyShares
|
|
5103
5320
|
};
|
|
5104
5321
|
}
|
|
5105
|
-
async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, initialSignerRules }) {
|
|
5322
|
+
async reshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, initialSignerRules }) {
|
|
5323
|
+
if (delegateToProjectEnvironment && offlineRecovery) {
|
|
5324
|
+
throw new Error('reshare: delegateToProjectEnvironment and offlineRecovery are mutually exclusive');
|
|
5325
|
+
}
|
|
5106
5326
|
// Fail-closed choke point: rules are only mintable on a share-set delegation
|
|
5107
5327
|
// reshare, never on the legacy 2/2 -> 2/3 path or revoke.
|
|
5108
5328
|
if (initialSignerRules && initialSignerRules.length > 0) {
|
|
@@ -5122,6 +5342,7 @@ class DynamicWalletClient {
|
|
|
5122
5342
|
sessionPublicKey,
|
|
5123
5343
|
cloudProviders,
|
|
5124
5344
|
delegateToProjectEnvironment,
|
|
5345
|
+
offlineRecovery,
|
|
5125
5346
|
mfaToken,
|
|
5126
5347
|
elevatedAccessToken,
|
|
5127
5348
|
revokeDelegation,
|
|
@@ -5516,6 +5737,19 @@ class DynamicWalletClient {
|
|
|
5516
5737
|
});
|
|
5517
5738
|
return;
|
|
5518
5739
|
}
|
|
5740
|
+
if (shareSetType === 'offlineRecovery' && newThresholdSignatureScheme) {
|
|
5741
|
+
// offlineRecovery activates its own share set, not rootUser: rootUser
|
|
5742
|
+
// stays the wallet's primary identity, so walletMap.shareSetId is never
|
|
5743
|
+
// touched here (unlike the rootUser branch below).
|
|
5744
|
+
this.recordOtherShareSet({
|
|
5745
|
+
accountAddress,
|
|
5746
|
+
wallet,
|
|
5747
|
+
newShareSetId: shareSetId,
|
|
5748
|
+
newThresholdSignatureScheme,
|
|
5749
|
+
shareSetType: 'offlineRecovery'
|
|
5750
|
+
});
|
|
5751
|
+
return;
|
|
5752
|
+
}
|
|
5519
5753
|
if (shareSetType !== 'rootUser') {
|
|
5520
5754
|
// 'delegated' (no newThresholdSignatureScheme, e.g. refresh) → SDK doesn't
|
|
5521
5755
|
// own the otherShareSets[] write from refresh.
|
|
@@ -5589,6 +5823,39 @@ class DynamicWalletClient {
|
|
|
5589
5823
|
shareSetType: 'delegated'
|
|
5590
5824
|
});
|
|
5591
5825
|
}
|
|
5826
|
+
/**
|
|
5827
|
+
* Advances the wallet's recorded scheme after a reshare. Offline recovery
|
|
5828
|
+
* mints a separate 2/2 set and leaves rootUser alone, so it must not write
|
|
5829
|
+
* here — same invariant {@link rotateRootUserShareSetIdIfChanged} applies to
|
|
5830
|
+
* `shareSetId`.
|
|
5831
|
+
*/ recordReshareThresholdScheme({ accountAddress, newThresholdSignatureScheme, offlineRecovery }) {
|
|
5832
|
+
if (offlineRecovery) return;
|
|
5833
|
+
this.updateWalletMap(accountAddress, {
|
|
5834
|
+
thresholdSignatureScheme: newThresholdSignatureScheme
|
|
5835
|
+
});
|
|
5836
|
+
}
|
|
5837
|
+
/**
|
|
5838
|
+
* Reverts the local state a failed reshare may have half-written: the
|
|
5839
|
+
* walletMap scheme bump, and the local client shares (wiped so the next
|
|
5840
|
+
* operation refetches a consistent set from backup).
|
|
5841
|
+
*
|
|
5842
|
+
* Skipped whenever the failed ceremony left rootUser's local state alone:
|
|
5843
|
+
* - public-key mismatch / 409 stale-shares preflight: `healStaleShares` just
|
|
5844
|
+
* restored these shares, and undoing that would break the caller's retry.
|
|
5845
|
+
* - offlineRecovery: mints a separate share set (`clientShares: []`) and
|
|
5846
|
+
* never rotates rootUser's shares or scheme — the mirror image of the
|
|
5847
|
+
* {@link recordReshareThresholdScheme} guard on the success path. Wiping
|
|
5848
|
+
* here would strand a wallet whose local keys are still valid.
|
|
5849
|
+
*/ async rollbackFailedReshareLocalState({ accountAddress, oldThresholdSignatureScheme, offlineRecovery, error }) {
|
|
5850
|
+
if (offlineRecovery || isPublicKeyMismatchError(error) || isStaleClientSharesError(error)) return;
|
|
5851
|
+
this.updateWalletMap(accountAddress, {
|
|
5852
|
+
thresholdSignatureScheme: oldThresholdSignatureScheme
|
|
5853
|
+
});
|
|
5854
|
+
await this.setClientKeySharesToStorage({
|
|
5855
|
+
accountAddress,
|
|
5856
|
+
clientKeyShares: []
|
|
5857
|
+
});
|
|
5858
|
+
}
|
|
5592
5859
|
// Inverse of `recordDelegatedShareSet`. The next delegateKeyShares no-op
|
|
5593
5860
|
// guard reads `otherShareSets`, so we have to clear it locally — `getWallet`
|
|
5594
5861
|
// short-circuits on cache hit and won't refresh from the server.
|
|
@@ -5617,7 +5884,7 @@ class DynamicWalletClient {
|
|
|
5617
5884
|
clientKeySharesBackupInfo: updatedBackupInfo
|
|
5618
5885
|
} : {}));
|
|
5619
5886
|
}
|
|
5620
|
-
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false, initialSignerRules }) {
|
|
5887
|
+
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, sessionPublicKey, cloudProviders = [], delegateToProjectEnvironment = false, offlineRecovery = false, mfaToken, elevatedAccessToken, revokeDelegation = false, googleDriveAccessToken, googleDriveTokenSource, staleShareRetry = false, initialSignerRules }) {
|
|
5621
5888
|
const dynamicRequestId = uuid.v4();
|
|
5622
5889
|
// Password validation - wrapped in try-catch for consistent error handling
|
|
5623
5890
|
// This path should NOT wipe key shares on failure (shares remain valid)
|
|
@@ -5732,7 +5999,7 @@ class DynamicWalletClient {
|
|
|
5732
5999
|
dynamicRequestId
|
|
5733
6000
|
}
|
|
5734
6001
|
});
|
|
5735
|
-
const data = await this.apiClient.reshare({
|
|
6002
|
+
const data = await this.apiClient.reshare(_extends({
|
|
5736
6003
|
walletId: wallet.walletId,
|
|
5737
6004
|
shareSetId: wallet.shareSetId,
|
|
5738
6005
|
clientKeygenIds: clientKeygenIds,
|
|
@@ -5743,10 +6010,13 @@ class DynamicWalletClient {
|
|
|
5743
6010
|
mfaToken,
|
|
5744
6011
|
elevatedAccessToken,
|
|
5745
6012
|
revokeDelegation,
|
|
5746
|
-
initialSignerRules
|
|
6013
|
+
initialSignerRules
|
|
6014
|
+
}, offlineRecovery ? {
|
|
6015
|
+
reshareKind: 'offlineRecovery'
|
|
6016
|
+
} : {}, {
|
|
5747
6017
|
onError: ceremonyGate.onError,
|
|
5748
6018
|
onCeremonyComplete: ceremonyGate.onCeremonyComplete
|
|
5749
|
-
});
|
|
6019
|
+
}));
|
|
5750
6020
|
this.logger.info('[WaasReshare] room_created received, starting local MPC', {
|
|
5751
6021
|
context: {
|
|
5752
6022
|
walletId: wallet.walletId,
|
|
@@ -5851,12 +6121,15 @@ class DynamicWalletClient {
|
|
|
5851
6121
|
}
|
|
5852
6122
|
const distribution = selectReshareDistribution({
|
|
5853
6123
|
resolvedDelegation,
|
|
6124
|
+
resolvedOfflineRecovery: offlineRecovery,
|
|
5854
6125
|
resolvedCloudProviders,
|
|
5855
6126
|
existingReshareResults,
|
|
5856
6127
|
newReshareResults
|
|
5857
6128
|
});
|
|
5858
|
-
this.
|
|
5859
|
-
|
|
6129
|
+
this.recordReshareThresholdScheme({
|
|
6130
|
+
accountAddress,
|
|
6131
|
+
newThresholdSignatureScheme,
|
|
6132
|
+
offlineRecovery
|
|
5860
6133
|
});
|
|
5861
6134
|
this.logger.info('[WaasReshare] local MPC done, awaiting ceremony_complete before backup', {
|
|
5862
6135
|
context: {
|
|
@@ -5951,6 +6224,7 @@ class DynamicWalletClient {
|
|
|
5951
6224
|
sessionPublicKey,
|
|
5952
6225
|
cloudProviders,
|
|
5953
6226
|
delegateToProjectEnvironment,
|
|
6227
|
+
offlineRecovery,
|
|
5954
6228
|
mfaToken,
|
|
5955
6229
|
elevatedAccessToken,
|
|
5956
6230
|
revokeDelegation,
|
|
@@ -5974,20 +6248,12 @@ class DynamicWalletClient {
|
|
|
5974
6248
|
dynamicRequestId
|
|
5975
6249
|
}
|
|
5976
6250
|
});
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
this.updateWalletMap(accountAddress, {
|
|
5984
|
-
thresholdSignatureScheme: oldThresholdSignatureScheme
|
|
5985
|
-
});
|
|
5986
|
-
await this.setClientKeySharesToStorage({
|
|
5987
|
-
accountAddress,
|
|
5988
|
-
clientKeyShares: []
|
|
5989
|
-
});
|
|
5990
|
-
}
|
|
6251
|
+
await this.rollbackFailedReshareLocalState({
|
|
6252
|
+
accountAddress,
|
|
6253
|
+
oldThresholdSignatureScheme,
|
|
6254
|
+
offlineRecovery,
|
|
6255
|
+
error
|
|
6256
|
+
});
|
|
5991
6257
|
throw error;
|
|
5992
6258
|
}
|
|
5993
6259
|
}
|
|
@@ -6143,6 +6409,27 @@ class DynamicWalletClient {
|
|
|
6143
6409
|
operationName: 'revokeDelegation'
|
|
6144
6410
|
});
|
|
6145
6411
|
}
|
|
6412
|
+
/**
|
|
6413
|
+
* Mints the dedicated 2/2 `offlineRecovery` share set. The
|
|
6414
|
+
* operation logic lives in `./offlineRecovery.ts` (cloud-provider-agnostic,
|
|
6415
|
+
* so iCloud / other providers slot in there); this method just drives it
|
|
6416
|
+
* with the client instance and returns the exported set's own backups.
|
|
6417
|
+
*/ async offlineRecovery({ accountAddress, password = undefined, signedSessionId, sessionPublicKey, mfaToken, cloudProvider = DEFAULT_OFFLINE_RECOVERY_CLOUD_PROVIDER, cloudProviderAccessToken }) {
|
|
6418
|
+
await performOfflineRecoveryOperation(this, {
|
|
6419
|
+
accountAddress,
|
|
6420
|
+
password,
|
|
6421
|
+
signedSessionId,
|
|
6422
|
+
sessionPublicKey,
|
|
6423
|
+
mfaToken,
|
|
6424
|
+
cloudProvider,
|
|
6425
|
+
cloudProviderAccessToken
|
|
6426
|
+
});
|
|
6427
|
+
// The exported set's own locations. Not `backups[cloudProvider]`: that is
|
|
6428
|
+
// rootUser's record for the provider, which this operation never writes.
|
|
6429
|
+
const backupInfo = this.getRequiredWalletFromMap(accountAddress).clientKeySharesBackupInfo;
|
|
6430
|
+
var _backupInfo_backups_BackupLocation_OFFLINE_RECOVERY;
|
|
6431
|
+
return (_backupInfo_backups_BackupLocation_OFFLINE_RECOVERY = backupInfo.backups[core.BackupLocation.OFFLINE_RECOVERY]) != null ? _backupInfo_backups_BackupLocation_OFFLINE_RECOVERY : [];
|
|
6432
|
+
}
|
|
6146
6433
|
createKeygenResult(mpcSigner, extractedPubkey, secretShare) {
|
|
6147
6434
|
if (mpcSigner instanceof web.Ecdsa) {
|
|
6148
6435
|
return new web.EcdsaKeygenResult(extractedPubkey, secretShare);
|
|
@@ -6799,7 +7086,7 @@ class DynamicWalletClient {
|
|
|
6799
7086
|
]);
|
|
6800
7087
|
}));
|
|
6801
7088
|
}
|
|
6802
|
-
async backupToCloudProvider({ provider, shares, accountAddress, password, chainName, bitcoinConfig, isPasswordEncrypted, preEncryptedShares, googleDriveAccessToken, googleDriveTokenSource, encryptionVersion }) {
|
|
7089
|
+
async backupToCloudProvider({ provider, shares, accountAddress, password, chainName, bitcoinConfig, isPasswordEncrypted, preEncryptedShares, googleDriveAccessToken, googleDriveTokenSource, encryptionVersion, isOfflineRecovery }) {
|
|
6803
7090
|
const encryptedCloudShares = preEncryptedShares != null ? preEncryptedShares : await Promise.all(shares.map((keyShare)=>this.encryptKeyShare({
|
|
6804
7091
|
keyShare,
|
|
6805
7092
|
password,
|
|
@@ -6809,7 +7096,8 @@ class DynamicWalletClient {
|
|
|
6809
7096
|
provider,
|
|
6810
7097
|
accountAddress,
|
|
6811
7098
|
encryptedKeyShares: encryptedCloudShares,
|
|
6812
|
-
googleDriveAccessToken
|
|
7099
|
+
googleDriveAccessToken,
|
|
7100
|
+
isOfflineRecovery
|
|
6813
7101
|
});
|
|
6814
7102
|
const keygenId = await this.computeBackupKeygenId({
|
|
6815
7103
|
chainName,
|
|
@@ -6992,7 +7280,8 @@ class DynamicWalletClient {
|
|
|
6992
7280
|
preEncryptedShares: encrypted,
|
|
6993
7281
|
googleDriveAccessToken,
|
|
6994
7282
|
googleDriveTokenSource,
|
|
6995
|
-
encryptionVersion
|
|
7283
|
+
encryptionVersion,
|
|
7284
|
+
isOfflineRecovery: distribution.isOfflineRecovery
|
|
6996
7285
|
}));
|
|
6997
7286
|
}
|
|
6998
7287
|
// When this ceremony minted a separate `delegated` share set (FF=on
|
|
@@ -7007,8 +7296,12 @@ class DynamicWalletClient {
|
|
|
7007
7296
|
// when `walletData` was captured at the top of this method and now.
|
|
7008
7297
|
const freshOtherShareSets = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets;
|
|
7009
7298
|
const delegatedShareSet = distribution.delegatedShare ? getDelegatedShareSet(freshOtherShareSets) : undefined;
|
|
7010
|
-
|
|
7011
|
-
|
|
7299
|
+
// Offline recovery activates its own share set, not rootUser: the
|
|
7300
|
+
// pending set must be the backup target or it is never activated and
|
|
7301
|
+
// the Drive location lands on rootUser instead.
|
|
7302
|
+
const offlineRecoveryShareSet = distribution.isOfflineRecovery ? getOfflineRecoveryShareSet(freshOtherShareSets) : undefined;
|
|
7303
|
+
var _delegatedShareSet_shareSetId, _ref;
|
|
7304
|
+
const targetShareSetId = (_ref = (_delegatedShareSet_shareSetId = delegatedShareSet == null ? void 0 : delegatedShareSet.shareSetId) != null ? _delegatedShareSet_shareSetId : offlineRecoveryShareSet == null ? void 0 : offlineRecoveryShareSet.shareSetId) != null ? _ref : walletData.shareSetId;
|
|
7012
7305
|
if (distribution.delegatedShare) {
|
|
7013
7306
|
uploadPromises.push(retryPromise(()=>this.publishDelegatedShare({
|
|
7014
7307
|
walletId: walletData.walletId,
|
|
@@ -7026,6 +7319,17 @@ class DynamicWalletClient {
|
|
|
7026
7319
|
}
|
|
7027
7320
|
const uploadResults = await Promise.all(uploadPromises);
|
|
7028
7321
|
const locations = uploadResults.filter((loc)=>loc !== undefined);
|
|
7322
|
+
// One physical Drive share, reported under two tags: `googleDrive` for
|
|
7323
|
+
// legacy backup-info reads and `offlineRecovery` for redcoast's
|
|
7324
|
+
// exported-set activation. Same keygenId/externalKeyShareId.
|
|
7325
|
+
if (distribution.isOfflineRecovery) {
|
|
7326
|
+
const googleDriveLocation = locations.find((location)=>location.location === core.BackupLocation.GOOGLE_DRIVE);
|
|
7327
|
+
if (googleDriveLocation) {
|
|
7328
|
+
locations.push(_extends({}, googleDriveLocation, {
|
|
7329
|
+
location: core.BackupLocation.OFFLINE_RECOVERY
|
|
7330
|
+
}));
|
|
7331
|
+
}
|
|
7332
|
+
}
|
|
7029
7333
|
this.logger.info('[backupSharesWithDistribution] Uploads complete, activating shares on server', _extends({}, logContext, {
|
|
7030
7334
|
locationCount: locations.length,
|
|
7031
7335
|
locations: locations.map((l)=>l.location)
|
|
@@ -7109,7 +7413,7 @@ class DynamicWalletClient {
|
|
|
7109
7413
|
});
|
|
7110
7414
|
}
|
|
7111
7415
|
var _backupData_locationsWithKeyShares2;
|
|
7112
|
-
const
|
|
7416
|
+
const activatedBackupInfo = getClientKeyShareBackupInfo({
|
|
7113
7417
|
walletProperties: {
|
|
7114
7418
|
derivationPath: walletData.derivationPath,
|
|
7115
7419
|
keyShares: ((_backupData_locationsWithKeyShares2 = backupData.locationsWithKeyShares) != null ? _backupData_locationsWithKeyShares2 : []).map((ks)=>({
|
|
@@ -7122,8 +7426,10 @@ class DynamicWalletClient {
|
|
|
7122
7426
|
thresholdSignatureScheme: walletData.thresholdSignatureScheme
|
|
7123
7427
|
}
|
|
7124
7428
|
});
|
|
7429
|
+
// offlineRecovery activates a separate share set, not rootUser's — record
|
|
7430
|
+
// it alongside rootUser's own backup info instead of replacing it.
|
|
7125
7431
|
this.updateWalletMap(accountAddress, {
|
|
7126
|
-
clientKeySharesBackupInfo:
|
|
7432
|
+
clientKeySharesBackupInfo: distribution.isOfflineRecovery ? recordOfflineRecoveryBackupLocations(walletData.clientKeySharesBackupInfo, activatedBackupInfo) : activatedBackupInfo
|
|
7127
7433
|
});
|
|
7128
7434
|
await this.storage.setItem(this.storageKey, JSON.stringify(this.walletMap));
|
|
7129
7435
|
var _backupData_locationsWithKeyShares_length, _backupData_locationsWithKeyShares_map;
|
|
@@ -7715,6 +8021,8 @@ class DynamicWalletClient {
|
|
|
7715
8021
|
throw error;
|
|
7716
8022
|
}
|
|
7717
8023
|
}
|
|
8024
|
+
// Public (was private) so the extracted offlineRecovery flow can resolve a
|
|
8025
|
+
// Google Drive token off the client instance — see ./offlineRecovery.ts.
|
|
7718
8026
|
async fetchGoogleDriveAccessToken(accountAddress) {
|
|
7719
8027
|
const oauthAccountId = await this.getGoogleOauthAccountIdOrThrow(accountAddress);
|
|
7720
8028
|
return this.apiClient.getAccessToken({
|
|
@@ -8199,13 +8507,14 @@ class DynamicWalletClient {
|
|
|
8199
8507
|
* @param encryptedKeyShares - Already encrypted key shares to upload
|
|
8200
8508
|
* @param googleDriveAccessToken - Optional Google OAuth access token used when provider is GOOGLE_DRIVE
|
|
8201
8509
|
* @returns Promise<void>
|
|
8202
|
-
*/ async uploadToCloudProvider({ provider, accountAddress, encryptedKeyShares, googleDriveAccessToken }) {
|
|
8510
|
+
*/ async uploadToCloudProvider({ provider, accountAddress, encryptedKeyShares, googleDriveAccessToken, isOfflineRecovery }) {
|
|
8203
8511
|
switch(provider){
|
|
8204
8512
|
case core.BackupLocation.GOOGLE_DRIVE:
|
|
8205
8513
|
return this.uploadKeySharesToGoogleDrive({
|
|
8206
8514
|
accountAddress,
|
|
8207
8515
|
encryptedKeyShares,
|
|
8208
|
-
googleDriveAccessToken
|
|
8516
|
+
googleDriveAccessToken,
|
|
8517
|
+
isOfflineRecovery
|
|
8209
8518
|
});
|
|
8210
8519
|
case core.BackupLocation.ICLOUD:
|
|
8211
8520
|
return this.uploadKeySharesToICloud({
|
|
@@ -8228,7 +8537,7 @@ class DynamicWalletClient {
|
|
|
8228
8537
|
* @param params.encryptedKeyShares - The specific key shares to upload to Google Drive
|
|
8229
8538
|
* @param params.googleDriveAccessToken - Google OAuth access token. Required; resolved upstream by `backupKeySharesToGoogleDrive` so the preflight can run on it.
|
|
8230
8539
|
* @returns Promise<string[]> - Array of Google Drive key share IDs that were uploaded
|
|
8231
|
-
*/ async uploadKeySharesToGoogleDrive({ accountAddress, encryptedKeyShares, googleDriveAccessToken }) {
|
|
8540
|
+
*/ async uploadKeySharesToGoogleDrive({ accountAddress, encryptedKeyShares, googleDriveAccessToken, isOfflineRecovery }) {
|
|
8232
8541
|
try {
|
|
8233
8542
|
if (encryptedKeyShares.length === 0) {
|
|
8234
8543
|
throw new Error('No key shares found');
|
|
@@ -8236,12 +8545,13 @@ class DynamicWalletClient {
|
|
|
8236
8545
|
if (!googleDriveAccessToken) {
|
|
8237
8546
|
throw new Error('googleDriveAccessToken is required for Google Drive upload');
|
|
8238
8547
|
}
|
|
8239
|
-
const walletData = this.
|
|
8548
|
+
const walletData = this.getRequiredWalletFromMap(accountAddress);
|
|
8240
8549
|
const thresholdSignatureScheme = walletData.thresholdSignatureScheme;
|
|
8241
8550
|
const fileName = getClientKeyShareExportFileName({
|
|
8242
8551
|
thresholdSignatureScheme,
|
|
8243
8552
|
accountAddress,
|
|
8244
|
-
isGoogleDrive: true
|
|
8553
|
+
isGoogleDrive: true,
|
|
8554
|
+
isOfflineRecovery
|
|
8245
8555
|
});
|
|
8246
8556
|
const backupData = createBackupData({
|
|
8247
8557
|
encryptedKeyShares,
|
|
@@ -8253,9 +8563,9 @@ class DynamicWalletClient {
|
|
|
8253
8563
|
fileName,
|
|
8254
8564
|
backupData,
|
|
8255
8565
|
accountAddress,
|
|
8256
|
-
walletId: walletData
|
|
8566
|
+
walletId: walletData.walletId,
|
|
8257
8567
|
environmentId: this.environmentId,
|
|
8258
|
-
chainName: walletData
|
|
8568
|
+
chainName: walletData.chainName,
|
|
8259
8569
|
logger: this.logger
|
|
8260
8570
|
});
|
|
8261
8571
|
return;
|
|
@@ -9635,6 +9945,8 @@ exports.ERROR_SIGN_MESSAGE = ERROR_SIGN_MESSAGE;
|
|
|
9635
9945
|
exports.ERROR_SIGN_TYPED_DATA = ERROR_SIGN_TYPED_DATA;
|
|
9636
9946
|
exports.ERROR_VERIFY_MESSAGE_SIGNATURE = ERROR_VERIFY_MESSAGE_SIGNATURE;
|
|
9637
9947
|
exports.ERROR_VERIFY_TRANSACTION_SIGNATURE = ERROR_VERIFY_TRANSACTION_SIGNATURE;
|
|
9948
|
+
exports.ExpiredGoogleDriveTokenError = ExpiredGoogleDriveTokenError;
|
|
9949
|
+
exports.GoogleDriveBackupBlockedError = GoogleDriveBackupBlockedError;
|
|
9638
9950
|
exports.HEAVY_QUEUE_OPERATIONS = HEAVY_QUEUE_OPERATIONS;
|
|
9639
9951
|
exports.OperationSource = OperationSource;
|
|
9640
9952
|
exports.RECOVER_QUEUE_OPERATIONS = RECOVER_QUEUE_OPERATIONS;
|
|
@@ -9656,6 +9968,7 @@ exports.createCloudProviderDistribution = createCloudProviderDistribution;
|
|
|
9656
9968
|
exports.createDelegationOnlyDistribution = createDelegationOnlyDistribution;
|
|
9657
9969
|
exports.createDelegationWithCloudProviderDistribution = createDelegationWithCloudProviderDistribution;
|
|
9658
9970
|
exports.createDynamicOnlyDistribution = createDynamicOnlyDistribution;
|
|
9971
|
+
exports.createOfflineRecoveryDistribution = createOfflineRecoveryDistribution;
|
|
9659
9972
|
exports.deleteICloudBackup = deleteICloudBackup;
|
|
9660
9973
|
exports.downloadStringAsFile = downloadStringAsFile;
|
|
9661
9974
|
exports.extractPubkey = extractPubkey;
|
|
@@ -9694,6 +10007,7 @@ exports.isStaleClientSharesError = isStaleClientSharesError;
|
|
|
9694
10007
|
exports.listICloudBackups = listICloudBackups;
|
|
9695
10008
|
exports.markCeremonyErrorNonRetryable = markCeremonyErrorNonRetryable;
|
|
9696
10009
|
exports.readEnvironmentSettings = readEnvironmentSettings;
|
|
10010
|
+
exports.recordOfflineRecoveryBackupLocations = recordOfflineRecoveryBackupLocations;
|
|
9697
10011
|
exports.resolveLogLevel = resolveLogLevel;
|
|
9698
10012
|
exports.retryPromise = retryPromise;
|
|
9699
10013
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|