@dynamic-labs-wallet/browser 1.0.97 → 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 +408 -68
- package/index.esm.js +404 -70
- 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 +43 -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 +49 -7
- 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_;
|
|
@@ -1313,7 +1334,8 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
1313
1334
|
[core.BackupLocation.ICLOUD]: [],
|
|
1314
1335
|
[core.BackupLocation.USER]: [],
|
|
1315
1336
|
[core.BackupLocation.EXTERNAL]: [],
|
|
1316
|
-
[core.BackupLocation.DELEGATED]: []
|
|
1337
|
+
[core.BackupLocation.DELEGATED]: [],
|
|
1338
|
+
[core.BackupLocation.OFFLINE_RECOVERY]: []
|
|
1317
1339
|
};
|
|
1318
1340
|
if (!(params == null ? void 0 : (_params_walletProperties = params.walletProperties) == null ? void 0 : _params_walletProperties.keyShares)) {
|
|
1319
1341
|
return {
|
|
@@ -1337,6 +1359,25 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
1337
1359
|
passwordEncrypted
|
|
1338
1360
|
};
|
|
1339
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
|
+
};
|
|
1340
1381
|
const timeoutPromise = ({ timeInMs, activity = 'Ceremony' })=>{
|
|
1341
1382
|
return new Promise((_, reject)=>setTimeout(()=>reject(new Error(`${activity} did not complete in ${timeInMs}ms`)), timeInMs));
|
|
1342
1383
|
};
|
|
@@ -2471,6 +2512,32 @@ class WalletBusyError extends Error {
|
|
|
2471
2512
|
this.accountAddress = accountAddress;
|
|
2472
2513
|
}
|
|
2473
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
|
+
}
|
|
2474
2541
|
/**
|
|
2475
2542
|
* Creates distribution where shares go to specified cloud providers
|
|
2476
2543
|
* Last share goes to cloud providers, rest to Dynamic backend
|
|
@@ -2527,11 +2594,22 @@ class WalletBusyError extends Error {
|
|
|
2527
2594
|
return ((_backupInfo_backups_provider_length = (_backupInfo_backups_provider = backupInfo.backups[provider]) == null ? void 0 : _backupInfo_backups_provider.length) != null ? _backupInfo_backups_provider_length : 0) > 0;
|
|
2528
2595
|
});
|
|
2529
2596
|
};
|
|
2597
|
+
/**
|
|
2598
|
+
* Backup locations that are NOT uploadable cloud providers. Anything returned by
|
|
2599
|
+
* {@link getActiveCloudProviders} is eventually dispatched through
|
|
2600
|
+
* `uploadToCloudProvider`, whose `default` branch throws — so a non-cloud
|
|
2601
|
+
* location leaking into that list turns the next backup/reshare into a hard
|
|
2602
|
+
* failure.
|
|
2603
|
+
*/ const NON_CLOUD_BACKUP_LOCATIONS = new Set([
|
|
2604
|
+
core.BackupLocation.DYNAMIC,
|
|
2605
|
+
core.BackupLocation.DELEGATED,
|
|
2606
|
+
core.BackupLocation.OFFLINE_RECOVERY
|
|
2607
|
+
]);
|
|
2530
2608
|
/**
|
|
2531
2609
|
* Gets all cloud providers that have backups for this wallet
|
|
2532
2610
|
*/ const getActiveCloudProviders = (backupInfo)=>{
|
|
2533
2611
|
if (!(backupInfo == null ? void 0 : backupInfo.backups)) return [];
|
|
2534
|
-
return Object.entries(backupInfo.backups).filter(([location, backups])
|
|
2612
|
+
return Object.entries(backupInfo.backups).filter(([location, backups])=>!NON_CLOUD_BACKUP_LOCATIONS.has(location) && backups.length > 0).map(([location])=>location);
|
|
2535
2613
|
};
|
|
2536
2614
|
const createDelegationOnlyDistribution = ({ existingShares, delegatedShare })=>({
|
|
2537
2615
|
clientShares: existingShares,
|
|
@@ -2543,24 +2621,41 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
2543
2621
|
cloudProviderShares: {}
|
|
2544
2622
|
});
|
|
2545
2623
|
/**
|
|
2546
|
-
*
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
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
|
+
});
|
|
2639
|
+
/**
|
|
2640
|
+
* Returns the share set of the given `shareSetType` from a wallet's
|
|
2641
|
+
* `otherShareSets` array, if one exists.
|
|
2549
2642
|
*
|
|
2550
|
-
* Invariant: at most one
|
|
2551
|
-
* returns more than one, the most recently created entry is returned and
|
|
2552
|
-
* warning is logged once on the call site (caller's responsibility — keep
|
|
2553
|
-
* helper pure).
|
|
2554
|
-
*/ const
|
|
2643
|
+
* Invariant: at most one entry per `shareSetType` per wallet. If the server
|
|
2644
|
+
* ever returns more than one, the most recently created entry is returned and
|
|
2645
|
+
* a warning is logged once on the call site (caller's responsibility — keep
|
|
2646
|
+
* this helper pure).
|
|
2647
|
+
*/ const getShareSet = (otherShareSets, shareSetType)=>{
|
|
2555
2648
|
var _otherShareSets_filter;
|
|
2556
|
-
const
|
|
2557
|
-
if (
|
|
2558
|
-
// Multiple
|
|
2559
|
-
// createdAt so behavior is deterministic if the invariant ever breaks.
|
|
2649
|
+
const matches = (_otherShareSets_filter = otherShareSets == null ? void 0 : otherShareSets.filter((shareSet)=>shareSet.shareSetType === shareSetType)) != null ? _otherShareSets_filter : [];
|
|
2650
|
+
if (matches.length <= 1) return matches[0];
|
|
2651
|
+
// Multiple entries of the same type shouldn't happen today. Pick the newest
|
|
2652
|
+
// by createdAt so behavior is deterministic if the invariant ever breaks.
|
|
2560
2653
|
return [
|
|
2561
|
-
...
|
|
2654
|
+
...matches
|
|
2562
2655
|
].sort((a, b)=>a.createdAt < b.createdAt ? 1 : -1)[0];
|
|
2563
2656
|
};
|
|
2657
|
+
/** Convenience wrapper around {@link getShareSet} for `'delegated'`. */ const getDelegatedShareSet = (otherShareSets)=>getShareSet(otherShareSets, 'delegated');
|
|
2658
|
+
/** Convenience wrapper around {@link getShareSet} for `'offlineRecovery'`. */ const getOfflineRecoveryShareSet = (otherShareSets)=>getShareSet(otherShareSets, 'offlineRecovery');
|
|
2564
2659
|
/**
|
|
2565
2660
|
* Checks whether a wallet has delegation set up. Two sources are checked
|
|
2566
2661
|
* because the SDK is in a transitional state:
|
|
@@ -2850,7 +2945,10 @@ const collectDynamicKeyShareIds = (locations)=>{
|
|
|
2850
2945
|
// ShareDistribution that storage/publish should follow. Lives outside the
|
|
2851
2946
|
// client class so it's straightforward to reason about and unit-test
|
|
2852
2947
|
// without spinning up DynamicWalletClient.
|
|
2853
|
-
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
|
+
}
|
|
2854
2952
|
const allClientShares = [
|
|
2855
2953
|
...existingReshareResults,
|
|
2856
2954
|
...newReshareResults
|
|
@@ -2879,6 +2977,19 @@ const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders,
|
|
|
2879
2977
|
delegatedShare: newReshareResults[0]
|
|
2880
2978
|
});
|
|
2881
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
|
+
}
|
|
2882
2993
|
if (resolvedCloudProviders.length > 0) {
|
|
2883
2994
|
return createCloudProviderDistribution({
|
|
2884
2995
|
providers: resolvedCloudProviders,
|
|
@@ -3046,6 +3157,123 @@ const readEnvironmentSettings = ()=>{
|
|
|
3046
3157
|
}
|
|
3047
3158
|
});
|
|
3048
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
|
+
|
|
3049
3277
|
/**
|
|
3050
3278
|
* Determines the recovery state of a wallet based on backup info and local shares.
|
|
3051
3279
|
*
|
|
@@ -3091,7 +3319,8 @@ const BACKUP_OPERATION = {
|
|
|
3091
3319
|
const KNOWN_SHARE_SET_TYPES = new Set([
|
|
3092
3320
|
'rootUser',
|
|
3093
3321
|
'delegated',
|
|
3094
|
-
'server'
|
|
3322
|
+
'server',
|
|
3323
|
+
'offlineRecovery'
|
|
3095
3324
|
]);
|
|
3096
3325
|
/**
|
|
3097
3326
|
* keygenIds the server records for the wallet's Dynamic-backed-up client
|
|
@@ -3177,7 +3406,9 @@ class DynamicWalletClient {
|
|
|
3177
3406
|
/**
|
|
3178
3407
|
* Get wallet properties from the wallet map using normalized address.
|
|
3179
3408
|
* Normalizes the address to lowercase for consistent lookups regardless of input casing.
|
|
3180
|
-
*/
|
|
3409
|
+
*/ // Public (was protected) so the extracted offlineRecovery flow can read
|
|
3410
|
+
// wallet state off the client instance — see ./offlineRecovery.ts.
|
|
3411
|
+
getWalletFromMap(accountAddress) {
|
|
3181
3412
|
return this.walletMap[normalizeAddress(accountAddress)];
|
|
3182
3413
|
}
|
|
3183
3414
|
/**
|
|
@@ -5088,7 +5319,10 @@ class DynamicWalletClient {
|
|
|
5088
5319
|
existingClientKeyShares
|
|
5089
5320
|
};
|
|
5090
5321
|
}
|
|
5091
|
-
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
|
+
}
|
|
5092
5326
|
// Fail-closed choke point: rules are only mintable on a share-set delegation
|
|
5093
5327
|
// reshare, never on the legacy 2/2 -> 2/3 path or revoke.
|
|
5094
5328
|
if (initialSignerRules && initialSignerRules.length > 0) {
|
|
@@ -5108,6 +5342,7 @@ class DynamicWalletClient {
|
|
|
5108
5342
|
sessionPublicKey,
|
|
5109
5343
|
cloudProviders,
|
|
5110
5344
|
delegateToProjectEnvironment,
|
|
5345
|
+
offlineRecovery,
|
|
5111
5346
|
mfaToken,
|
|
5112
5347
|
elevatedAccessToken,
|
|
5113
5348
|
revokeDelegation,
|
|
@@ -5502,6 +5737,19 @@ class DynamicWalletClient {
|
|
|
5502
5737
|
});
|
|
5503
5738
|
return;
|
|
5504
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
|
+
}
|
|
5505
5753
|
if (shareSetType !== 'rootUser') {
|
|
5506
5754
|
// 'delegated' (no newThresholdSignatureScheme, e.g. refresh) → SDK doesn't
|
|
5507
5755
|
// own the otherShareSets[] write from refresh.
|
|
@@ -5533,38 +5781,81 @@ class DynamicWalletClient {
|
|
|
5533
5781
|
shareSetId
|
|
5534
5782
|
});
|
|
5535
5783
|
}
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5784
|
+
/**
|
|
5785
|
+
* Records a share set of `shareSetType` under `wallet.otherShareSets[]`,
|
|
5786
|
+
* replacing an existing entry of that type rather than appending a duplicate
|
|
5787
|
+
* on retry. Only reachable from ceremony flows that thread
|
|
5788
|
+
* `newThresholdSignatureScheme`; refresh doesn't, so it skips before here.
|
|
5789
|
+
*/ recordOtherShareSet({ accountAddress, wallet, newShareSetId, newThresholdSignatureScheme, shareSetType }) {
|
|
5542
5790
|
const walletProps = this.getWalletFromMap(accountAddress);
|
|
5543
5791
|
var _walletProps_otherShareSets;
|
|
5544
5792
|
const existing = (_walletProps_otherShareSets = walletProps == null ? void 0 : walletProps.otherShareSets) != null ? _walletProps_otherShareSets : [];
|
|
5545
5793
|
const newEntry = {
|
|
5546
5794
|
shareSetId: newShareSetId,
|
|
5547
|
-
shareSetType
|
|
5795
|
+
shareSetType,
|
|
5548
5796
|
thresholdSignatureScheme: newThresholdSignatureScheme,
|
|
5549
5797
|
createdAt: new Date().toISOString()
|
|
5550
5798
|
};
|
|
5551
|
-
const
|
|
5552
|
-
const updated =
|
|
5799
|
+
const existingIdx = existing.findIndex((s)=>s.shareSetType === shareSetType);
|
|
5800
|
+
const updated = existingIdx >= 0 ? existing.map((s, i)=>i === existingIdx ? newEntry : s) : [
|
|
5553
5801
|
...existing,
|
|
5554
5802
|
newEntry
|
|
5555
5803
|
];
|
|
5556
|
-
this.logger.info(
|
|
5804
|
+
this.logger.info(`[WaasShareSet] ${shareSetType} shareSetId added by reshare ceremony`, {
|
|
5557
5805
|
context: {
|
|
5558
5806
|
walletId: wallet.walletId,
|
|
5559
5807
|
rootUserShareSetId: wallet.shareSetId,
|
|
5560
|
-
|
|
5561
|
-
|
|
5808
|
+
shareSetType,
|
|
5809
|
+
newShareSetId,
|
|
5810
|
+
replacedExisting: existingIdx >= 0
|
|
5562
5811
|
}
|
|
5563
5812
|
});
|
|
5564
5813
|
this.updateWalletMap(accountAddress, {
|
|
5565
5814
|
otherShareSets: updated
|
|
5566
5815
|
});
|
|
5567
5816
|
}
|
|
5817
|
+
/** Delegation-path wrapper over {@link recordOtherShareSet}, so the delegation call sites are unchanged. */ recordDelegatedShareSet({ accountAddress, wallet, newShareSetId, newThresholdSignatureScheme }) {
|
|
5818
|
+
this.recordOtherShareSet({
|
|
5819
|
+
accountAddress,
|
|
5820
|
+
wallet,
|
|
5821
|
+
newShareSetId,
|
|
5822
|
+
newThresholdSignatureScheme,
|
|
5823
|
+
shareSetType: 'delegated'
|
|
5824
|
+
});
|
|
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
|
+
}
|
|
5568
5859
|
// Inverse of `recordDelegatedShareSet`. The next delegateKeyShares no-op
|
|
5569
5860
|
// guard reads `otherShareSets`, so we have to clear it locally — `getWallet`
|
|
5570
5861
|
// short-circuits on cache hit and won't refresh from the server.
|
|
@@ -5593,7 +5884,7 @@ class DynamicWalletClient {
|
|
|
5593
5884
|
clientKeySharesBackupInfo: updatedBackupInfo
|
|
5594
5885
|
} : {}));
|
|
5595
5886
|
}
|
|
5596
|
-
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 }) {
|
|
5597
5888
|
const dynamicRequestId = uuid.v4();
|
|
5598
5889
|
// Password validation - wrapped in try-catch for consistent error handling
|
|
5599
5890
|
// This path should NOT wipe key shares on failure (shares remain valid)
|
|
@@ -5708,7 +5999,7 @@ class DynamicWalletClient {
|
|
|
5708
5999
|
dynamicRequestId
|
|
5709
6000
|
}
|
|
5710
6001
|
});
|
|
5711
|
-
const data = await this.apiClient.reshare({
|
|
6002
|
+
const data = await this.apiClient.reshare(_extends({
|
|
5712
6003
|
walletId: wallet.walletId,
|
|
5713
6004
|
shareSetId: wallet.shareSetId,
|
|
5714
6005
|
clientKeygenIds: clientKeygenIds,
|
|
@@ -5719,10 +6010,13 @@ class DynamicWalletClient {
|
|
|
5719
6010
|
mfaToken,
|
|
5720
6011
|
elevatedAccessToken,
|
|
5721
6012
|
revokeDelegation,
|
|
5722
|
-
initialSignerRules
|
|
6013
|
+
initialSignerRules
|
|
6014
|
+
}, offlineRecovery ? {
|
|
6015
|
+
reshareKind: 'offlineRecovery'
|
|
6016
|
+
} : {}, {
|
|
5723
6017
|
onError: ceremonyGate.onError,
|
|
5724
6018
|
onCeremonyComplete: ceremonyGate.onCeremonyComplete
|
|
5725
|
-
});
|
|
6019
|
+
}));
|
|
5726
6020
|
this.logger.info('[WaasReshare] room_created received, starting local MPC', {
|
|
5727
6021
|
context: {
|
|
5728
6022
|
walletId: wallet.walletId,
|
|
@@ -5827,12 +6121,15 @@ class DynamicWalletClient {
|
|
|
5827
6121
|
}
|
|
5828
6122
|
const distribution = selectReshareDistribution({
|
|
5829
6123
|
resolvedDelegation,
|
|
6124
|
+
resolvedOfflineRecovery: offlineRecovery,
|
|
5830
6125
|
resolvedCloudProviders,
|
|
5831
6126
|
existingReshareResults,
|
|
5832
6127
|
newReshareResults
|
|
5833
6128
|
});
|
|
5834
|
-
this.
|
|
5835
|
-
|
|
6129
|
+
this.recordReshareThresholdScheme({
|
|
6130
|
+
accountAddress,
|
|
6131
|
+
newThresholdSignatureScheme,
|
|
6132
|
+
offlineRecovery
|
|
5836
6133
|
});
|
|
5837
6134
|
this.logger.info('[WaasReshare] local MPC done, awaiting ceremony_complete before backup', {
|
|
5838
6135
|
context: {
|
|
@@ -5927,6 +6224,7 @@ class DynamicWalletClient {
|
|
|
5927
6224
|
sessionPublicKey,
|
|
5928
6225
|
cloudProviders,
|
|
5929
6226
|
delegateToProjectEnvironment,
|
|
6227
|
+
offlineRecovery,
|
|
5930
6228
|
mfaToken,
|
|
5931
6229
|
elevatedAccessToken,
|
|
5932
6230
|
revokeDelegation,
|
|
@@ -5950,20 +6248,12 @@ class DynamicWalletClient {
|
|
|
5950
6248
|
dynamicRequestId
|
|
5951
6249
|
}
|
|
5952
6250
|
});
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
this.updateWalletMap(accountAddress, {
|
|
5960
|
-
thresholdSignatureScheme: oldThresholdSignatureScheme
|
|
5961
|
-
});
|
|
5962
|
-
await this.setClientKeySharesToStorage({
|
|
5963
|
-
accountAddress,
|
|
5964
|
-
clientKeyShares: []
|
|
5965
|
-
});
|
|
5966
|
-
}
|
|
6251
|
+
await this.rollbackFailedReshareLocalState({
|
|
6252
|
+
accountAddress,
|
|
6253
|
+
oldThresholdSignatureScheme,
|
|
6254
|
+
offlineRecovery,
|
|
6255
|
+
error
|
|
6256
|
+
});
|
|
5967
6257
|
throw error;
|
|
5968
6258
|
}
|
|
5969
6259
|
}
|
|
@@ -6119,6 +6409,27 @@ class DynamicWalletClient {
|
|
|
6119
6409
|
operationName: 'revokeDelegation'
|
|
6120
6410
|
});
|
|
6121
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
|
+
}
|
|
6122
6433
|
createKeygenResult(mpcSigner, extractedPubkey, secretShare) {
|
|
6123
6434
|
if (mpcSigner instanceof web.Ecdsa) {
|
|
6124
6435
|
return new web.EcdsaKeygenResult(extractedPubkey, secretShare);
|
|
@@ -6775,7 +7086,7 @@ class DynamicWalletClient {
|
|
|
6775
7086
|
]);
|
|
6776
7087
|
}));
|
|
6777
7088
|
}
|
|
6778
|
-
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 }) {
|
|
6779
7090
|
const encryptedCloudShares = preEncryptedShares != null ? preEncryptedShares : await Promise.all(shares.map((keyShare)=>this.encryptKeyShare({
|
|
6780
7091
|
keyShare,
|
|
6781
7092
|
password,
|
|
@@ -6785,7 +7096,8 @@ class DynamicWalletClient {
|
|
|
6785
7096
|
provider,
|
|
6786
7097
|
accountAddress,
|
|
6787
7098
|
encryptedKeyShares: encryptedCloudShares,
|
|
6788
|
-
googleDriveAccessToken
|
|
7099
|
+
googleDriveAccessToken,
|
|
7100
|
+
isOfflineRecovery
|
|
6789
7101
|
});
|
|
6790
7102
|
const keygenId = await this.computeBackupKeygenId({
|
|
6791
7103
|
chainName,
|
|
@@ -6968,7 +7280,8 @@ class DynamicWalletClient {
|
|
|
6968
7280
|
preEncryptedShares: encrypted,
|
|
6969
7281
|
googleDriveAccessToken,
|
|
6970
7282
|
googleDriveTokenSource,
|
|
6971
|
-
encryptionVersion
|
|
7283
|
+
encryptionVersion,
|
|
7284
|
+
isOfflineRecovery: distribution.isOfflineRecovery
|
|
6972
7285
|
}));
|
|
6973
7286
|
}
|
|
6974
7287
|
// When this ceremony minted a separate `delegated` share set (FF=on
|
|
@@ -6983,8 +7296,12 @@ class DynamicWalletClient {
|
|
|
6983
7296
|
// when `walletData` was captured at the top of this method and now.
|
|
6984
7297
|
const freshOtherShareSets = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets;
|
|
6985
7298
|
const delegatedShareSet = distribution.delegatedShare ? getDelegatedShareSet(freshOtherShareSets) : undefined;
|
|
6986
|
-
|
|
6987
|
-
|
|
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;
|
|
6988
7305
|
if (distribution.delegatedShare) {
|
|
6989
7306
|
uploadPromises.push(retryPromise(()=>this.publishDelegatedShare({
|
|
6990
7307
|
walletId: walletData.walletId,
|
|
@@ -7002,6 +7319,17 @@ class DynamicWalletClient {
|
|
|
7002
7319
|
}
|
|
7003
7320
|
const uploadResults = await Promise.all(uploadPromises);
|
|
7004
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
|
+
}
|
|
7005
7333
|
this.logger.info('[backupSharesWithDistribution] Uploads complete, activating shares on server', _extends({}, logContext, {
|
|
7006
7334
|
locationCount: locations.length,
|
|
7007
7335
|
locations: locations.map((l)=>l.location)
|
|
@@ -7085,7 +7413,7 @@ class DynamicWalletClient {
|
|
|
7085
7413
|
});
|
|
7086
7414
|
}
|
|
7087
7415
|
var _backupData_locationsWithKeyShares2;
|
|
7088
|
-
const
|
|
7416
|
+
const activatedBackupInfo = getClientKeyShareBackupInfo({
|
|
7089
7417
|
walletProperties: {
|
|
7090
7418
|
derivationPath: walletData.derivationPath,
|
|
7091
7419
|
keyShares: ((_backupData_locationsWithKeyShares2 = backupData.locationsWithKeyShares) != null ? _backupData_locationsWithKeyShares2 : []).map((ks)=>({
|
|
@@ -7098,8 +7426,10 @@ class DynamicWalletClient {
|
|
|
7098
7426
|
thresholdSignatureScheme: walletData.thresholdSignatureScheme
|
|
7099
7427
|
}
|
|
7100
7428
|
});
|
|
7429
|
+
// offlineRecovery activates a separate share set, not rootUser's — record
|
|
7430
|
+
// it alongside rootUser's own backup info instead of replacing it.
|
|
7101
7431
|
this.updateWalletMap(accountAddress, {
|
|
7102
|
-
clientKeySharesBackupInfo:
|
|
7432
|
+
clientKeySharesBackupInfo: distribution.isOfflineRecovery ? recordOfflineRecoveryBackupLocations(walletData.clientKeySharesBackupInfo, activatedBackupInfo) : activatedBackupInfo
|
|
7103
7433
|
});
|
|
7104
7434
|
await this.storage.setItem(this.storageKey, JSON.stringify(this.walletMap));
|
|
7105
7435
|
var _backupData_locationsWithKeyShares_length, _backupData_locationsWithKeyShares_map;
|
|
@@ -7691,6 +8021,8 @@ class DynamicWalletClient {
|
|
|
7691
8021
|
throw error;
|
|
7692
8022
|
}
|
|
7693
8023
|
}
|
|
8024
|
+
// Public (was private) so the extracted offlineRecovery flow can resolve a
|
|
8025
|
+
// Google Drive token off the client instance — see ./offlineRecovery.ts.
|
|
7694
8026
|
async fetchGoogleDriveAccessToken(accountAddress) {
|
|
7695
8027
|
const oauthAccountId = await this.getGoogleOauthAccountIdOrThrow(accountAddress);
|
|
7696
8028
|
return this.apiClient.getAccessToken({
|
|
@@ -8175,13 +8507,14 @@ class DynamicWalletClient {
|
|
|
8175
8507
|
* @param encryptedKeyShares - Already encrypted key shares to upload
|
|
8176
8508
|
* @param googleDriveAccessToken - Optional Google OAuth access token used when provider is GOOGLE_DRIVE
|
|
8177
8509
|
* @returns Promise<void>
|
|
8178
|
-
*/ async uploadToCloudProvider({ provider, accountAddress, encryptedKeyShares, googleDriveAccessToken }) {
|
|
8510
|
+
*/ async uploadToCloudProvider({ provider, accountAddress, encryptedKeyShares, googleDriveAccessToken, isOfflineRecovery }) {
|
|
8179
8511
|
switch(provider){
|
|
8180
8512
|
case core.BackupLocation.GOOGLE_DRIVE:
|
|
8181
8513
|
return this.uploadKeySharesToGoogleDrive({
|
|
8182
8514
|
accountAddress,
|
|
8183
8515
|
encryptedKeyShares,
|
|
8184
|
-
googleDriveAccessToken
|
|
8516
|
+
googleDriveAccessToken,
|
|
8517
|
+
isOfflineRecovery
|
|
8185
8518
|
});
|
|
8186
8519
|
case core.BackupLocation.ICLOUD:
|
|
8187
8520
|
return this.uploadKeySharesToICloud({
|
|
@@ -8204,7 +8537,7 @@ class DynamicWalletClient {
|
|
|
8204
8537
|
* @param params.encryptedKeyShares - The specific key shares to upload to Google Drive
|
|
8205
8538
|
* @param params.googleDriveAccessToken - Google OAuth access token. Required; resolved upstream by `backupKeySharesToGoogleDrive` so the preflight can run on it.
|
|
8206
8539
|
* @returns Promise<string[]> - Array of Google Drive key share IDs that were uploaded
|
|
8207
|
-
*/ async uploadKeySharesToGoogleDrive({ accountAddress, encryptedKeyShares, googleDriveAccessToken }) {
|
|
8540
|
+
*/ async uploadKeySharesToGoogleDrive({ accountAddress, encryptedKeyShares, googleDriveAccessToken, isOfflineRecovery }) {
|
|
8208
8541
|
try {
|
|
8209
8542
|
if (encryptedKeyShares.length === 0) {
|
|
8210
8543
|
throw new Error('No key shares found');
|
|
@@ -8212,12 +8545,13 @@ class DynamicWalletClient {
|
|
|
8212
8545
|
if (!googleDriveAccessToken) {
|
|
8213
8546
|
throw new Error('googleDriveAccessToken is required for Google Drive upload');
|
|
8214
8547
|
}
|
|
8215
|
-
const walletData = this.
|
|
8548
|
+
const walletData = this.getRequiredWalletFromMap(accountAddress);
|
|
8216
8549
|
const thresholdSignatureScheme = walletData.thresholdSignatureScheme;
|
|
8217
8550
|
const fileName = getClientKeyShareExportFileName({
|
|
8218
8551
|
thresholdSignatureScheme,
|
|
8219
8552
|
accountAddress,
|
|
8220
|
-
isGoogleDrive: true
|
|
8553
|
+
isGoogleDrive: true,
|
|
8554
|
+
isOfflineRecovery
|
|
8221
8555
|
});
|
|
8222
8556
|
const backupData = createBackupData({
|
|
8223
8557
|
encryptedKeyShares,
|
|
@@ -8229,9 +8563,9 @@ class DynamicWalletClient {
|
|
|
8229
8563
|
fileName,
|
|
8230
8564
|
backupData,
|
|
8231
8565
|
accountAddress,
|
|
8232
|
-
walletId: walletData
|
|
8566
|
+
walletId: walletData.walletId,
|
|
8233
8567
|
environmentId: this.environmentId,
|
|
8234
|
-
chainName: walletData
|
|
8568
|
+
chainName: walletData.chainName,
|
|
8235
8569
|
logger: this.logger
|
|
8236
8570
|
});
|
|
8237
8571
|
return;
|
|
@@ -9611,6 +9945,8 @@ exports.ERROR_SIGN_MESSAGE = ERROR_SIGN_MESSAGE;
|
|
|
9611
9945
|
exports.ERROR_SIGN_TYPED_DATA = ERROR_SIGN_TYPED_DATA;
|
|
9612
9946
|
exports.ERROR_VERIFY_MESSAGE_SIGNATURE = ERROR_VERIFY_MESSAGE_SIGNATURE;
|
|
9613
9947
|
exports.ERROR_VERIFY_TRANSACTION_SIGNATURE = ERROR_VERIFY_TRANSACTION_SIGNATURE;
|
|
9948
|
+
exports.ExpiredGoogleDriveTokenError = ExpiredGoogleDriveTokenError;
|
|
9949
|
+
exports.GoogleDriveBackupBlockedError = GoogleDriveBackupBlockedError;
|
|
9614
9950
|
exports.HEAVY_QUEUE_OPERATIONS = HEAVY_QUEUE_OPERATIONS;
|
|
9615
9951
|
exports.OperationSource = OperationSource;
|
|
9616
9952
|
exports.RECOVER_QUEUE_OPERATIONS = RECOVER_QUEUE_OPERATIONS;
|
|
@@ -9632,6 +9968,7 @@ exports.createCloudProviderDistribution = createCloudProviderDistribution;
|
|
|
9632
9968
|
exports.createDelegationOnlyDistribution = createDelegationOnlyDistribution;
|
|
9633
9969
|
exports.createDelegationWithCloudProviderDistribution = createDelegationWithCloudProviderDistribution;
|
|
9634
9970
|
exports.createDynamicOnlyDistribution = createDynamicOnlyDistribution;
|
|
9971
|
+
exports.createOfflineRecoveryDistribution = createOfflineRecoveryDistribution;
|
|
9635
9972
|
exports.deleteICloudBackup = deleteICloudBackup;
|
|
9636
9973
|
exports.downloadStringAsFile = downloadStringAsFile;
|
|
9637
9974
|
exports.extractPubkey = extractPubkey;
|
|
@@ -9650,6 +9987,8 @@ exports.getHttpStatus = getHttpStatus;
|
|
|
9650
9987
|
exports.getICloudBackup = getICloudBackup;
|
|
9651
9988
|
exports.getMPCSignatureScheme = getMPCSignatureScheme;
|
|
9652
9989
|
exports.getMPCSigner = getMPCSigner;
|
|
9990
|
+
exports.getOfflineRecoveryShareSet = getOfflineRecoveryShareSet;
|
|
9991
|
+
exports.getShareSet = getShareSet;
|
|
9653
9992
|
exports.hasCloudProviderBackup = hasCloudProviderBackup;
|
|
9654
9993
|
exports.hasDelegatedBackup = hasDelegatedBackup;
|
|
9655
9994
|
exports.hasEncryptedSharesCached = hasEncryptedSharesCached;
|
|
@@ -9668,6 +10007,7 @@ exports.isStaleClientSharesError = isStaleClientSharesError;
|
|
|
9668
10007
|
exports.listICloudBackups = listICloudBackups;
|
|
9669
10008
|
exports.markCeremonyErrorNonRetryable = markCeremonyErrorNonRetryable;
|
|
9670
10009
|
exports.readEnvironmentSettings = readEnvironmentSettings;
|
|
10010
|
+
exports.recordOfflineRecoveryBackupLocations = recordOfflineRecoveryBackupLocations;
|
|
9671
10011
|
exports.resolveLogLevel = resolveLogLevel;
|
|
9672
10012
|
exports.retryPromise = retryPromise;
|
|
9673
10013
|
exports.shouldReshareToSameBackups = shouldReshareToSameBackups;
|