@dynamic-labs-wallet/browser 1.0.19 → 1.0.21
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 +286 -235
- package/index.esm.js +287 -236
- package/package.json +3 -3
- package/src/client.d.ts +11 -5
- package/src/client.d.ts.map +1 -1
- package/src/httpStatus.d.ts +8 -0
- package/src/httpStatus.d.ts.map +1 -0
- package/src/services/signedSession.d.ts +51 -0
- package/src/services/signedSession.d.ts.map +1 -0
- package/src/utils.d.ts +2 -7
- package/src/utils.d.ts.map +1 -1
package/index.cjs
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@dynamic-labs-wallet/core');
|
|
4
4
|
var web = require('#internal/web');
|
|
5
|
-
var semver = require('semver');
|
|
6
5
|
var uuid = require('uuid');
|
|
7
6
|
var primitives = require('@dynamic-labs-wallet/primitives');
|
|
8
7
|
var sdkApiCore = require('@dynamic-labs/sdk-api-core');
|
|
9
8
|
var loadArgon2idWasm = require('argon2id');
|
|
10
9
|
var axios = require('axios');
|
|
11
10
|
var PQueue = require('p-queue');
|
|
11
|
+
var semver = require('semver');
|
|
12
12
|
|
|
13
13
|
function _extends() {
|
|
14
14
|
_extends = Object.assign || function assign(target) {
|
|
@@ -997,6 +997,16 @@ const downloadFileFromGoogleDrive = async ({ accessToken, fileName })=>{
|
|
|
997
997
|
}
|
|
998
998
|
};
|
|
999
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* Extracts the HTTP status from an `AxiosError`, or `undefined` for non-HTTP
|
|
1002
|
+
* errors. Lives in its own leaf module (no `#internal/web` / WASM imports) so
|
|
1003
|
+
* lightweight consumers like `services/signedSession.ts` can use it without
|
|
1004
|
+
* pulling the MPC WASM bundle that `utils.ts` loads.
|
|
1005
|
+
*/ const getHttpStatus = (error)=>{
|
|
1006
|
+
var _error_response;
|
|
1007
|
+
return error instanceof axios.AxiosError ? (_error_response = error.response) == null ? void 0 : _error_response.status : undefined;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1000
1010
|
const STORAGE_KEY = 'dynamic-waas-wallet-client';
|
|
1001
1011
|
const BACKUP_FILENAME = 'dynamicWalletKeyShareBackup.json';
|
|
1002
1012
|
const CLIENT_KEYSHARE_EXPORT_FILENAME_PREFIX = 'dynamicWalletKeyShareBackup';
|
|
@@ -1076,14 +1086,6 @@ const getClientKeyShareBackupInfo = (params)=>{
|
|
|
1076
1086
|
const timeoutPromise = ({ timeInMs, activity = 'Ceremony' })=>{
|
|
1077
1087
|
return new Promise((_, reject)=>setTimeout(()=>reject(new Error(`${activity} did not complete in ${timeInMs}ms`)), timeInMs));
|
|
1078
1088
|
};
|
|
1079
|
-
/**
|
|
1080
|
-
* Extracts the HTTP status code from an error, returning undefined for non-HTTP
|
|
1081
|
-
* failures (network errors, DNS, aborts) which retry logic typically treats
|
|
1082
|
-
* the same as 5xx.
|
|
1083
|
-
*/ const getHttpStatus = (error)=>{
|
|
1084
|
-
var _error_response;
|
|
1085
|
-
return error instanceof axios.AxiosError ? (_error_response = error.response) == null ? void 0 : _error_response.status : undefined;
|
|
1086
|
-
};
|
|
1087
1089
|
const buildErrorContext = (error)=>{
|
|
1088
1090
|
var _error_response, _error_response1;
|
|
1089
1091
|
return {
|
|
@@ -2463,6 +2465,84 @@ const readEnvironmentSettings = ()=>{
|
|
|
2463
2465
|
}
|
|
2464
2466
|
});
|
|
2465
2467
|
|
|
2468
|
+
/**
|
|
2469
|
+
* Owns everything about signed sessions and their single-use replay nonces:
|
|
2470
|
+
* resolving a session from an explicit value or the host reverse channel,
|
|
2471
|
+
* deciding whether the SDK version requires one, and building the
|
|
2472
|
+
* refresh-on-replay-400 retry predicate shared by the backup and recovery
|
|
2473
|
+
* flows.
|
|
2474
|
+
*
|
|
2475
|
+
* Extracted from `DynamicWalletClient` so this logic can be exercised in
|
|
2476
|
+
* isolation. The host callback is read lazily (`getCallback`) so it stays in
|
|
2477
|
+
* sync with the owning client even if reassigned after construction.
|
|
2478
|
+
*/ class SignedSessionManager {
|
|
2479
|
+
/** True when a reverse channel is configured to mint fresh signed sessions. */ get canRefresh() {
|
|
2480
|
+
return this.getCallback() !== undefined;
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* Resolves the signed session ID from an explicit value or falls back to the
|
|
2484
|
+
* host reverse channel. Throws if neither source provides a value.
|
|
2485
|
+
*/ async resolve(signedSessionId) {
|
|
2486
|
+
const callback = this.getCallback();
|
|
2487
|
+
const resolved = signedSessionId != null ? signedSessionId : await (callback == null ? void 0 : callback());
|
|
2488
|
+
if (!resolved) {
|
|
2489
|
+
throw new Error(signedSessionId === undefined && callback ? 'signedSessionId callback returned an invalid value' : 'signedSessionId is required for backup but was not provided and no callback is configured');
|
|
2490
|
+
}
|
|
2491
|
+
return resolved;
|
|
2492
|
+
}
|
|
2493
|
+
/** Whether the configured SDK version requires a signed session ID. */ isRequired() {
|
|
2494
|
+
return this.required;
|
|
2495
|
+
}
|
|
2496
|
+
computeIsRequired(sdkVersion) {
|
|
2497
|
+
if (!sdkVersion) {
|
|
2498
|
+
return false;
|
|
2499
|
+
}
|
|
2500
|
+
try {
|
|
2501
|
+
const parsedVersion = core.parseNamespacedVersion(sdkVersion);
|
|
2502
|
+
if (!parsedVersion) {
|
|
2503
|
+
return false;
|
|
2504
|
+
}
|
|
2505
|
+
const { namespace, version } = parsedVersion;
|
|
2506
|
+
const namespaceMinVersion = SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE[namespace];
|
|
2507
|
+
if (namespaceMinVersion) {
|
|
2508
|
+
return semver.gte(version, namespaceMinVersion);
|
|
2509
|
+
}
|
|
2510
|
+
return semver.gte(version, SIGNED_SESSION_ID_MIN_VERSION);
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
this.logger.warn(`[DynamicWaasWalletClient] Error checking if requiresSignedSessionId should be set to true for version ${sdkVersion}`, {
|
|
2513
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2514
|
+
});
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Builds a `retryPromise` `shouldRetry` predicate that refreshes a consumed
|
|
2520
|
+
* single-use nonce via the reverse channel on a replay 400 and retries once.
|
|
2521
|
+
* The signed session carries a single-use replay nonce: it is valid on its
|
|
2522
|
+
* first use, but once consumed (e.g. by a preceding operation) the server
|
|
2523
|
+
* rejects with a 400, at which point a fresh session is fetched and the call
|
|
2524
|
+
* retried. `alsoRetry` lets callers opt into additional retryable statuses
|
|
2525
|
+
* (e.g. 429 / 5xx) on top of the nonce refresh.
|
|
2526
|
+
*/ refreshShouldRetry({ onRefreshed, operationName, logContext, alsoRetry }) {
|
|
2527
|
+
return async (error, attempt)=>{
|
|
2528
|
+
const status = getHttpStatus(error);
|
|
2529
|
+
if (status === 400 && attempt === 1 && this.canRefresh) {
|
|
2530
|
+
onRefreshed(await this.resolve());
|
|
2531
|
+
this.logger.info(`[${operationName}] refreshed signed session, retrying`, _extends({
|
|
2532
|
+
status
|
|
2533
|
+
}, logContext));
|
|
2534
|
+
return true;
|
|
2535
|
+
}
|
|
2536
|
+
return alsoRetry ? alsoRetry(status) : false;
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
constructor({ logger, sdkVersion, getCallback }){
|
|
2540
|
+
this.logger = logger;
|
|
2541
|
+
this.getCallback = getCallback;
|
|
2542
|
+
this.required = this.computeIsRequired(sdkVersion);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2466
2546
|
/**
|
|
2467
2547
|
* Determines the recovery state of a wallet based on backup info and local shares.
|
|
2468
2548
|
*
|
|
@@ -2512,17 +2592,6 @@ const KNOWN_SHARE_SET_TYPES = new Set([
|
|
|
2512
2592
|
]);
|
|
2513
2593
|
class DynamicWalletClient {
|
|
2514
2594
|
/**
|
|
2515
|
-
* Resolves the signed session ID from an explicit value or falls back to the callback.
|
|
2516
|
-
* Throws if neither source provides a value.
|
|
2517
|
-
*/ async resolveSignedSessionId(signedSessionId) {
|
|
2518
|
-
const resolved = signedSessionId != null ? signedSessionId : await (this.getSignedSessionIdCallback == null ? void 0 : this.getSignedSessionIdCallback.call(this));
|
|
2519
|
-
if (!resolved) {
|
|
2520
|
-
const errorMsg = signedSessionId === undefined && this.getSignedSessionIdCallback ? 'signedSessionId callback returned an invalid value' : 'signedSessionId is required for backup but was not provided and no callback is configured';
|
|
2521
|
-
throw new Error(errorMsg);
|
|
2522
|
-
}
|
|
2523
|
-
return resolved;
|
|
2524
|
-
}
|
|
2525
|
-
/**
|
|
2526
2595
|
* Check if wallet has heavy operations in progress
|
|
2527
2596
|
*/ static isHeavyOpInProgress(accountAddress) {
|
|
2528
2597
|
return WalletQueueManager.isHeavyOpInProgress(accountAddress);
|
|
@@ -2608,29 +2677,8 @@ class DynamicWalletClient {
|
|
|
2608
2677
|
* Check if the SDK version meets the requirement for signed session ID
|
|
2609
2678
|
* Uses namespace-specific version requirements when available
|
|
2610
2679
|
* @returns boolean indicating if requireSignedSessionId should be set to true
|
|
2611
|
-
*/ requiresSignedSessionId() {
|
|
2612
|
-
|
|
2613
|
-
return false;
|
|
2614
|
-
}
|
|
2615
|
-
try {
|
|
2616
|
-
const parsedVersion = core.parseNamespacedVersion(this.sdkVersion);
|
|
2617
|
-
if (!parsedVersion) {
|
|
2618
|
-
return false;
|
|
2619
|
-
}
|
|
2620
|
-
const { namespace, version } = parsedVersion;
|
|
2621
|
-
// Check if we have a namespace-specific version requirement
|
|
2622
|
-
const namespaceMinVersion = SIGNED_SESSION_ID_MIN_VERSION_BY_NAMESPACE[namespace];
|
|
2623
|
-
if (namespaceMinVersion) {
|
|
2624
|
-
return semver.gte(version, namespaceMinVersion);
|
|
2625
|
-
}
|
|
2626
|
-
// Fall back to default version requirement
|
|
2627
|
-
return semver.gte(version, SIGNED_SESSION_ID_MIN_VERSION);
|
|
2628
|
-
} catch (error) {
|
|
2629
|
-
this.logger.warn(`[DynamicWaasWalletClient] Error checking if requiresSignedSessionId should be set to true for version ${this.sdkVersion}`, {
|
|
2630
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2631
|
-
});
|
|
2632
|
-
return false;
|
|
2633
|
-
}
|
|
2680
|
+
*/ /** @see SignedSessionManager.isRequired */ requiresSignedSessionId() {
|
|
2681
|
+
return this.signedSession.isRequired();
|
|
2634
2682
|
}
|
|
2635
2683
|
async initLoggerContext(authToken) {
|
|
2636
2684
|
// only decode jwt token in header auth mode
|
|
@@ -3665,24 +3713,35 @@ class DynamicWalletClient {
|
|
|
3665
3713
|
});
|
|
3666
3714
|
// Ensure client key shares exist before hitting the API
|
|
3667
3715
|
const clientKeyShares = await this.ensureClientShare(accountAddress);
|
|
3668
|
-
//
|
|
3669
|
-
//
|
|
3670
|
-
//
|
|
3671
|
-
|
|
3672
|
-
//
|
|
3673
|
-
//
|
|
3674
|
-
//
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3716
|
+
// The MPC ceremony needs client + server in the room concurrently, so we
|
|
3717
|
+
// run mpcSigner.refresh BEFORE awaiting the gate. The gate's purpose is to
|
|
3718
|
+
// ensure the walletMap.shareSetId rotation (done in onCeremonyComplete)
|
|
3719
|
+
// lands before storeEncryptedBackupByWallet reads it; without it, backup
|
|
3720
|
+
// would race ceremony_complete and the atomic swap could target the stale
|
|
3721
|
+
// row. refresh's API call returns on room_created (not the local MPC
|
|
3722
|
+
// result), so the gate await sits between the local MPC and the backup.
|
|
3723
|
+
const ceremonyGate = this.createCeremonyTerminalGate((_accountAddress, _walletId, shareSetId, shareSetType)=>{
|
|
3724
|
+
this.logger.info('[WaasRefresh] ceremony_complete received', {
|
|
3725
|
+
context: {
|
|
3726
|
+
walletId: wallet.walletId,
|
|
3727
|
+
newShareSetId: shareSetId,
|
|
3728
|
+
shareSetType,
|
|
3729
|
+
dynamicRequestId
|
|
3730
|
+
}
|
|
3731
|
+
});
|
|
3732
|
+
this.logOnMalformedCeremonyPayload({
|
|
3733
|
+
walletId: wallet.walletId,
|
|
3734
|
+
ceremony: 'refresh',
|
|
3735
|
+
shareSetId,
|
|
3736
|
+
shareSetType
|
|
3737
|
+
});
|
|
3738
|
+
this.rotateRootUserShareSetIdIfChanged({
|
|
3739
|
+
accountAddress,
|
|
3740
|
+
wallet,
|
|
3741
|
+
ceremony: 'refresh',
|
|
3742
|
+
shareSetId,
|
|
3743
|
+
shareSetType
|
|
3744
|
+
});
|
|
3686
3745
|
});
|
|
3687
3746
|
this.logger.info('[WaasRefresh] calling refreshWalletAccountShares', {
|
|
3688
3747
|
context: {
|
|
@@ -3697,31 +3756,8 @@ class DynamicWalletClient {
|
|
|
3697
3756
|
shareSetId: wallet.shareSetId,
|
|
3698
3757
|
mfaToken,
|
|
3699
3758
|
elevatedAccessToken,
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
this.logger.info('[WaasRefresh] ceremony_complete received', {
|
|
3703
|
-
context: {
|
|
3704
|
-
walletId: wallet.walletId,
|
|
3705
|
-
newShareSetId: shareSetId,
|
|
3706
|
-
shareSetType,
|
|
3707
|
-
dynamicRequestId
|
|
3708
|
-
}
|
|
3709
|
-
});
|
|
3710
|
-
this.logOnMalformedCeremonyPayload({
|
|
3711
|
-
walletId: wallet.walletId,
|
|
3712
|
-
ceremony: 'refresh',
|
|
3713
|
-
shareSetId,
|
|
3714
|
-
shareSetType
|
|
3715
|
-
});
|
|
3716
|
-
this.rotateRootUserShareSetIdIfChanged({
|
|
3717
|
-
accountAddress,
|
|
3718
|
-
wallet,
|
|
3719
|
-
ceremony: 'refresh',
|
|
3720
|
-
shareSetId,
|
|
3721
|
-
shareSetType
|
|
3722
|
-
});
|
|
3723
|
-
ceremonyCompleteResolver(undefined);
|
|
3724
|
-
}
|
|
3759
|
+
onError: ceremonyGate.onError,
|
|
3760
|
+
onCeremonyComplete: ceremonyGate.onCeremonyComplete
|
|
3725
3761
|
});
|
|
3726
3762
|
this.logger.info('[WaasRefresh] room_created received, starting local MPC', {
|
|
3727
3763
|
context: {
|
|
@@ -3751,40 +3787,11 @@ class DynamicWalletClient {
|
|
|
3751
3787
|
this.logger.info('[WaasRefresh] local MPC done, awaiting ceremony_complete before backup', {
|
|
3752
3788
|
context: {
|
|
3753
3789
|
walletId: wallet.walletId,
|
|
3754
|
-
ceremonyCallbackFired,
|
|
3790
|
+
ceremonyCallbackFired: ceremonyGate.fired,
|
|
3755
3791
|
dynamicRequestId
|
|
3756
3792
|
}
|
|
3757
3793
|
});
|
|
3758
|
-
|
|
3759
|
-
// onCeremonyComplete lands in walletMap before the backup call reads it.
|
|
3760
|
-
// Bounded by a 5s timeout so a missing/late event never hangs the SDK —
|
|
3761
|
-
// if it fires we proceed with the rotated id; if it doesn't we fall back
|
|
3762
|
-
// to the (stale) walletMap.shareSetId, matching pre-fix behavior.
|
|
3763
|
-
const REFRESH_CEREMONY_COMPLETE_TIMEOUT_MS = 5000;
|
|
3764
|
-
let ceremonyCompleteTimedOut = false;
|
|
3765
|
-
let refreshTimeoutId;
|
|
3766
|
-
await Promise.race([
|
|
3767
|
-
ceremonyCompletePromise,
|
|
3768
|
-
new Promise((resolve)=>{
|
|
3769
|
-
refreshTimeoutId = setTimeout(()=>{
|
|
3770
|
-
ceremonyCompleteTimedOut = true;
|
|
3771
|
-
resolve(undefined);
|
|
3772
|
-
}, REFRESH_CEREMONY_COMPLETE_TIMEOUT_MS);
|
|
3773
|
-
})
|
|
3774
|
-
]);
|
|
3775
|
-
// Cancel the pending timer when ceremony_complete wins the race so it
|
|
3776
|
-
// doesn't fire later and flip ceremonyCompleteTimedOut after the fact.
|
|
3777
|
-
// Safe no-op when the timer has already fired.
|
|
3778
|
-
clearTimeout(refreshTimeoutId);
|
|
3779
|
-
if (ceremonyCompleteTimedOut) {
|
|
3780
|
-
this.logger.warn(`[WaasRefresh] ceremony_complete didn’t arrive within ${REFRESH_CEREMONY_COMPLETE_TIMEOUT_MS}ms — proceeding with stale walletMap`, {
|
|
3781
|
-
context: {
|
|
3782
|
-
walletId: wallet.walletId,
|
|
3783
|
-
ceremonyCallbackFired,
|
|
3784
|
-
dynamicRequestId
|
|
3785
|
-
}
|
|
3786
|
-
});
|
|
3787
|
-
}
|
|
3794
|
+
await ceremonyGate.awaitTerminal();
|
|
3788
3795
|
// Backup & activate server shares BEFORE saving to local storage.
|
|
3789
3796
|
// This prevents a mismatch where new client shares are stored locally
|
|
3790
3797
|
// but old server shares remain active if backup/activation fails.
|
|
@@ -3807,15 +3814,13 @@ class DynamicWalletClient {
|
|
|
3807
3814
|
clientKeyShares: refreshResults
|
|
3808
3815
|
});
|
|
3809
3816
|
// Operation summary — correlates against downstream sign failures /
|
|
3810
|
-
// pending-rotation reports.
|
|
3811
|
-
//
|
|
3812
|
-
// SSE event was lost mid-flight.
|
|
3817
|
+
// pending-rotation reports. The gate always fired here (we throw before
|
|
3818
|
+
// backup otherwise); kept for log continuity.
|
|
3813
3819
|
this.logger.info('[WaasRefresh] completed', {
|
|
3814
3820
|
context: {
|
|
3815
3821
|
walletId: wallet.walletId,
|
|
3816
3822
|
oldShareSetId: wallet.shareSetId,
|
|
3817
|
-
ceremonyCallbackFired,
|
|
3818
|
-
ceremonyCompleteTimedOut,
|
|
3823
|
+
ceremonyCallbackFired: ceremonyGate.fired,
|
|
3819
3824
|
dynamicRequestId
|
|
3820
3825
|
}
|
|
3821
3826
|
});
|
|
@@ -3971,6 +3976,50 @@ class DynamicWalletClient {
|
|
|
3971
3976
|
});
|
|
3972
3977
|
}
|
|
3973
3978
|
}
|
|
3979
|
+
// Terminal gate shared by refresh and reshare. Both APIs resolve early on
|
|
3980
|
+
// room_created, so a failed ceremony surfaces *after* the API call returns —
|
|
3981
|
+
// as an SSE error event, a stream close without ceremony_complete, or a
|
|
3982
|
+
// transport drop (all delivered via onError). awaitTerminal() races the two
|
|
3983
|
+
// outcomes with no timeout:
|
|
3984
|
+
// - ceremony_complete → resolves → caller backs up against the rotated id.
|
|
3985
|
+
// - any onError → rejects → caller throws, never backs up (the wedge fix:
|
|
3986
|
+
// a failed ceremony must not promote a client share whose server
|
|
3987
|
+
// counterpart was never persisted).
|
|
3988
|
+
// redcoast always emits one or the other and the event-stream consumer turns
|
|
3989
|
+
// "closed without ceremony_complete" into an onError, so the race always
|
|
3990
|
+
// settles. Aborting fails closed — worst case is a clean retry.
|
|
3991
|
+
//
|
|
3992
|
+
// The reject handle is wired synchronously (Promise executors run inline)
|
|
3993
|
+
// before the API call starts, so an immediate onError can never be dropped.
|
|
3994
|
+
// The lost branch's rejection is swallowed so it never surfaces as unhandled.
|
|
3995
|
+
createCeremonyTerminalGate(onCeremonyComplete) {
|
|
3996
|
+
let resolveComplete;
|
|
3997
|
+
const completePromise = new Promise((resolve)=>{
|
|
3998
|
+
resolveComplete = resolve;
|
|
3999
|
+
});
|
|
4000
|
+
let rejectOnError;
|
|
4001
|
+
const errorPromise = new Promise((_, reject)=>{
|
|
4002
|
+
rejectOnError = reject;
|
|
4003
|
+
});
|
|
4004
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4005
|
+
errorPromise.catch(()=>{});
|
|
4006
|
+
let fired = false;
|
|
4007
|
+
return {
|
|
4008
|
+
onError: rejectOnError,
|
|
4009
|
+
onCeremonyComplete: (accountAddress, walletId, shareSetId, shareSetType)=>{
|
|
4010
|
+
fired = true;
|
|
4011
|
+
onCeremonyComplete == null ? void 0 : onCeremonyComplete(accountAddress, walletId, shareSetId, shareSetType);
|
|
4012
|
+
resolveComplete();
|
|
4013
|
+
},
|
|
4014
|
+
awaitTerminal: ()=>Promise.race([
|
|
4015
|
+
completePromise,
|
|
4016
|
+
errorPromise
|
|
4017
|
+
]),
|
|
4018
|
+
get fired () {
|
|
4019
|
+
return fired;
|
|
4020
|
+
}
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
3974
4023
|
// Runs the post-ceremony backup-activation step (storeEncryptedBackupByWallet
|
|
3975
4024
|
// for refresh, backupSharesWithDistribution for reshare). On failure, rolls
|
|
3976
4025
|
// walletMap.shareSetId back to the source row.
|
|
@@ -4202,19 +4251,39 @@ class DynamicWalletClient {
|
|
|
4202
4251
|
...existingClientKeygenIds
|
|
4203
4252
|
];
|
|
4204
4253
|
const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
|
|
4205
|
-
//
|
|
4206
|
-
//
|
|
4207
|
-
// fire" with downstream sign failures or pending-rotation reports.
|
|
4208
|
-
let ceremonyCallbackFired = false;
|
|
4209
|
-
// Deferred resolved by onCeremonyComplete. The MPC reshare requires
|
|
4210
|
-
// client + server in the room concurrently, so we run mpcSigner.reshare*
|
|
4211
|
-
// BEFORE awaiting this — the await's purpose is to ensure the
|
|
4254
|
+
// The MPC reshare needs client + server in the room concurrently, so we
|
|
4255
|
+
// run mpcSigner.reshare* BEFORE awaiting the gate. The gate ensures the
|
|
4212
4256
|
// walletMap.shareSetId rotation lands before storeEncryptedBackupByWallet
|
|
4213
|
-
// reads it
|
|
4214
|
-
// swap
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4257
|
+
// reads it; without it, backup would race ceremony_complete and the atomic
|
|
4258
|
+
// swap could target the stale row.
|
|
4259
|
+
const ceremonyGate = this.createCeremonyTerminalGate((_accountAddress, _walletId, shareSetId, shareSetType)=>{
|
|
4260
|
+
this.logger.info('[WaasReshare] ceremony_complete received', {
|
|
4261
|
+
context: {
|
|
4262
|
+
walletId: wallet.walletId,
|
|
4263
|
+
newShareSetId: shareSetId,
|
|
4264
|
+
shareSetType,
|
|
4265
|
+
dynamicRequestId
|
|
4266
|
+
}
|
|
4267
|
+
});
|
|
4268
|
+
this.logOnMalformedCeremonyPayload({
|
|
4269
|
+
walletId: wallet.walletId,
|
|
4270
|
+
ceremony: 'reshare',
|
|
4271
|
+
shareSetId,
|
|
4272
|
+
shareSetType
|
|
4273
|
+
});
|
|
4274
|
+
this.rotateRootUserShareSetIdIfChanged({
|
|
4275
|
+
accountAddress,
|
|
4276
|
+
wallet,
|
|
4277
|
+
ceremony: 'reshare',
|
|
4278
|
+
shareSetId,
|
|
4279
|
+
shareSetType,
|
|
4280
|
+
newThresholdSignatureScheme,
|
|
4281
|
+
extraContext: {
|
|
4282
|
+
oldThresholdSignatureScheme,
|
|
4283
|
+
newThresholdSignatureScheme,
|
|
4284
|
+
resolvedDelegation
|
|
4285
|
+
}
|
|
4286
|
+
});
|
|
4218
4287
|
});
|
|
4219
4288
|
this.logger.info('[WaasReshare] calling reshare API', {
|
|
4220
4289
|
context: {
|
|
@@ -4237,37 +4306,8 @@ class DynamicWalletClient {
|
|
|
4237
4306
|
mfaToken,
|
|
4238
4307
|
elevatedAccessToken,
|
|
4239
4308
|
revokeDelegation,
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
this.logger.info('[WaasReshare] ceremony_complete received', {
|
|
4243
|
-
context: {
|
|
4244
|
-
walletId: wallet.walletId,
|
|
4245
|
-
newShareSetId: shareSetId,
|
|
4246
|
-
shareSetType,
|
|
4247
|
-
dynamicRequestId
|
|
4248
|
-
}
|
|
4249
|
-
});
|
|
4250
|
-
this.logOnMalformedCeremonyPayload({
|
|
4251
|
-
walletId: wallet.walletId,
|
|
4252
|
-
ceremony: 'reshare',
|
|
4253
|
-
shareSetId,
|
|
4254
|
-
shareSetType
|
|
4255
|
-
});
|
|
4256
|
-
this.rotateRootUserShareSetIdIfChanged({
|
|
4257
|
-
accountAddress,
|
|
4258
|
-
wallet,
|
|
4259
|
-
ceremony: 'reshare',
|
|
4260
|
-
shareSetId,
|
|
4261
|
-
shareSetType,
|
|
4262
|
-
newThresholdSignatureScheme,
|
|
4263
|
-
extraContext: {
|
|
4264
|
-
oldThresholdSignatureScheme,
|
|
4265
|
-
newThresholdSignatureScheme,
|
|
4266
|
-
resolvedDelegation
|
|
4267
|
-
}
|
|
4268
|
-
});
|
|
4269
|
-
ceremonyCompleteResolver(undefined);
|
|
4270
|
-
}
|
|
4309
|
+
onError: ceremonyGate.onError,
|
|
4310
|
+
onCeremonyComplete: ceremonyGate.onCeremonyComplete
|
|
4271
4311
|
});
|
|
4272
4312
|
this.logger.info('[WaasReshare] room_created received, starting local MPC', {
|
|
4273
4313
|
context: {
|
|
@@ -4376,38 +4416,11 @@ class DynamicWalletClient {
|
|
|
4376
4416
|
this.logger.info('[WaasReshare] local MPC done, awaiting ceremony_complete before backup', {
|
|
4377
4417
|
context: {
|
|
4378
4418
|
walletId: wallet.walletId,
|
|
4379
|
-
ceremonyCallbackFired,
|
|
4419
|
+
ceremonyCallbackFired: ceremonyGate.fired,
|
|
4380
4420
|
dynamicRequestId
|
|
4381
4421
|
}
|
|
4382
4422
|
});
|
|
4383
|
-
|
|
4384
|
-
// onCeremonyComplete lands in walletMap before the backup call reads it.
|
|
4385
|
-
// Bounded by a 5s timeout so a missing/late event never hangs the SDK.
|
|
4386
|
-
const RESHARE_CEREMONY_COMPLETE_TIMEOUT_MS = 5000;
|
|
4387
|
-
let ceremonyCompleteTimedOut = false;
|
|
4388
|
-
let reshareTimeoutId;
|
|
4389
|
-
await Promise.race([
|
|
4390
|
-
ceremonyCompletePromise,
|
|
4391
|
-
new Promise((resolve)=>{
|
|
4392
|
-
reshareTimeoutId = setTimeout(()=>{
|
|
4393
|
-
ceremonyCompleteTimedOut = true;
|
|
4394
|
-
resolve(undefined);
|
|
4395
|
-
}, RESHARE_CEREMONY_COMPLETE_TIMEOUT_MS);
|
|
4396
|
-
})
|
|
4397
|
-
]);
|
|
4398
|
-
// Cancel the pending timer when ceremony_complete wins the race so it
|
|
4399
|
-
// doesn't fire later and flip ceremonyCompleteTimedOut after the fact.
|
|
4400
|
-
// Safe no-op when the timer has already fired.
|
|
4401
|
-
clearTimeout(reshareTimeoutId);
|
|
4402
|
-
if (ceremonyCompleteTimedOut) {
|
|
4403
|
-
this.logger.warn(`[WaasReshare] ceremony_complete didn’t arrive within ${RESHARE_CEREMONY_COMPLETE_TIMEOUT_MS}ms — proceeding with stale walletMap`, {
|
|
4404
|
-
context: {
|
|
4405
|
-
walletId: wallet.walletId,
|
|
4406
|
-
ceremonyCallbackFired,
|
|
4407
|
-
dynamicRequestId
|
|
4408
|
-
}
|
|
4409
|
-
});
|
|
4410
|
-
}
|
|
4423
|
+
await ceremonyGate.awaitTerminal();
|
|
4411
4424
|
// Backup & activate server shares BEFORE saving to local storage.
|
|
4412
4425
|
// This prevents a mismatch where new client shares are stored locally
|
|
4413
4426
|
// but old server shares remain active if backup/activation fails.
|
|
@@ -4445,18 +4458,16 @@ class DynamicWalletClient {
|
|
|
4445
4458
|
});
|
|
4446
4459
|
}
|
|
4447
4460
|
// Operation summary — correlates against downstream sign failures /
|
|
4448
|
-
// pending-rotation reports.
|
|
4449
|
-
//
|
|
4450
|
-
// SSE event was lost mid-flight.
|
|
4461
|
+
// pending-rotation reports. The gate always fired here (we throw before
|
|
4462
|
+
// backup otherwise); kept for log continuity.
|
|
4451
4463
|
this.logger.info('[WaasReshare] completed', {
|
|
4452
4464
|
context: {
|
|
4453
|
-
ceremonyCompleteTimedOut,
|
|
4454
4465
|
walletId: wallet.walletId,
|
|
4455
4466
|
oldShareSetId: wallet.shareSetId,
|
|
4456
4467
|
oldThresholdSignatureScheme,
|
|
4457
4468
|
newThresholdSignatureScheme,
|
|
4458
4469
|
resolvedDelegation,
|
|
4459
|
-
ceremonyCallbackFired
|
|
4470
|
+
ceremonyCallbackFired: ceremonyGate.fired
|
|
4460
4471
|
}
|
|
4461
4472
|
});
|
|
4462
4473
|
} catch (error) {
|
|
@@ -5296,8 +5307,7 @@ class DynamicWalletClient {
|
|
|
5296
5307
|
try {
|
|
5297
5308
|
var _preEncryptedCloudShares_, _this_getWalletFromMap, _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
5298
5309
|
// `let` so the retry path can swap in a freshly-signed session via the reverse channel on 400.
|
|
5299
|
-
let resolvedSignedSessionId = await this.
|
|
5300
|
-
const canRefreshSignedSessionId = this.getSignedSessionIdCallback !== undefined;
|
|
5310
|
+
let resolvedSignedSessionId = await this.signedSession.resolve(signedSessionId);
|
|
5301
5311
|
if (!(walletData == null ? void 0 : walletData.walletId)) {
|
|
5302
5312
|
const error = new Error(`WalletId not found for accountAddress ${accountAddress}`);
|
|
5303
5313
|
logError({
|
|
@@ -5390,19 +5400,18 @@ class DynamicWalletClient {
|
|
|
5390
5400
|
passwordUpdateBatchId
|
|
5391
5401
|
}), {
|
|
5392
5402
|
operationName,
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
}
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
}
|
|
5403
|
+
// Refresh a consumed nonce on a 400; otherwise back off and retry
|
|
5404
|
+
// transient statuses (undefined / 429 / 5xx).
|
|
5405
|
+
shouldRetry: this.signedSession.refreshShouldRetry({
|
|
5406
|
+
onRefreshed: (id)=>{
|
|
5407
|
+
resolvedSignedSessionId = id;
|
|
5408
|
+
},
|
|
5409
|
+
operationName,
|
|
5410
|
+
logContext: {
|
|
5411
|
+
dynamicRequestId
|
|
5412
|
+
},
|
|
5413
|
+
alsoRetry: (status)=>status === undefined || status === 429 || status !== undefined && status >= 500
|
|
5414
|
+
})
|
|
5406
5415
|
}));
|
|
5407
5416
|
}
|
|
5408
5417
|
// Cloud providers already have internal retries (Google Drive and iCloud),
|
|
@@ -5592,7 +5601,7 @@ class DynamicWalletClient {
|
|
|
5592
5601
|
// still throw — the host can re-auth from the error.
|
|
5593
5602
|
let resolvedSessionId;
|
|
5594
5603
|
try {
|
|
5595
|
-
resolvedSessionId = await this.
|
|
5604
|
+
resolvedSessionId = await this.signedSession.resolve(signedSessionId);
|
|
5596
5605
|
} catch (e) {
|
|
5597
5606
|
resolvedSessionId = undefined;
|
|
5598
5607
|
}
|
|
@@ -5950,13 +5959,14 @@ class DynamicWalletClient {
|
|
|
5950
5959
|
externalKeyShareIds: shares[core.BackupLocation.DYNAMIC] || []
|
|
5951
5960
|
}
|
|
5952
5961
|
});
|
|
5953
|
-
const result = await this.
|
|
5962
|
+
const result = await this.recoverEncryptedSharesWithNonceRetry({
|
|
5954
5963
|
walletId: wallet.walletId,
|
|
5955
5964
|
shareSetId: wallet.shareSetId,
|
|
5956
5965
|
externalKeyShareIds: shares[core.BackupLocation.DYNAMIC] || [],
|
|
5957
|
-
signedSessionId
|
|
5958
|
-
|
|
5959
|
-
|
|
5966
|
+
signedSessionId
|
|
5967
|
+
}, {
|
|
5968
|
+
walletId: wallet.walletId,
|
|
5969
|
+
context: 'validatePasswordAgainstEncryptedShares'
|
|
5960
5970
|
});
|
|
5961
5971
|
if (!result) {
|
|
5962
5972
|
throw new Error('Failed to retrieve encrypted backup from API for password validation');
|
|
@@ -6147,6 +6157,32 @@ class DynamicWalletClient {
|
|
|
6147
6157
|
mfaToken
|
|
6148
6158
|
}));
|
|
6149
6159
|
}
|
|
6160
|
+
/**
|
|
6161
|
+
* Fetches encrypted backup shares from the server, transparently refreshing
|
|
6162
|
+
* the signed session on a single-use-nonce rejection. Recovery is otherwise
|
|
6163
|
+
* non-retrying, so other errors surface immediately. When no reverse channel
|
|
6164
|
+
* is configured the provided session is used unchanged.
|
|
6165
|
+
*/ async recoverEncryptedSharesWithNonceRetry(params, logContext) {
|
|
6166
|
+
let resolvedSignedSessionId = params.signedSessionId;
|
|
6167
|
+
return retryPromise(()=>this.apiClient.recoverEncryptedBackupByWallet(_extends({}, params, {
|
|
6168
|
+
signedSessionId: resolvedSignedSessionId,
|
|
6169
|
+
requiresSignedSessionId: this.requiresSignedSessionId(),
|
|
6170
|
+
userId: this.userId
|
|
6171
|
+
})), {
|
|
6172
|
+
operationName: 'recoverEncryptedBackupByWallet',
|
|
6173
|
+
// At most one retry — the nonce refresh — and no back-off before it,
|
|
6174
|
+
// since a consumed-nonce 400 is deterministic, not a transient error.
|
|
6175
|
+
maxAttempts: 2,
|
|
6176
|
+
retryInterval: 0,
|
|
6177
|
+
shouldRetry: this.signedSession.refreshShouldRetry({
|
|
6178
|
+
onRefreshed: (id)=>{
|
|
6179
|
+
resolvedSignedSessionId = id;
|
|
6180
|
+
},
|
|
6181
|
+
operationName: 'recoverEncryptedBackupByWallet',
|
|
6182
|
+
logContext
|
|
6183
|
+
})
|
|
6184
|
+
});
|
|
6185
|
+
}
|
|
6150
6186
|
async internalRecoverEncryptedBackupByWallet({ accountAddress, password, walletOperation, signedSessionId, shareCount = undefined, storeRecoveredShares = true, mfaToken }) {
|
|
6151
6187
|
try {
|
|
6152
6188
|
const wallet = this.getWalletFromMap(accountAddress);
|
|
@@ -6167,14 +6203,15 @@ class DynamicWalletClient {
|
|
|
6167
6203
|
thresholdSignatureScheme: wallet.thresholdSignatureScheme
|
|
6168
6204
|
}
|
|
6169
6205
|
});
|
|
6170
|
-
const data = await this.
|
|
6206
|
+
const data = await this.recoverEncryptedSharesWithNonceRetry({
|
|
6171
6207
|
walletId: wallet.walletId,
|
|
6172
6208
|
shareSetId: wallet.shareSetId,
|
|
6173
6209
|
externalKeyShareIds: dynamicKeyShareIds,
|
|
6174
6210
|
signedSessionId,
|
|
6175
|
-
mfaToken
|
|
6176
|
-
|
|
6177
|
-
|
|
6211
|
+
mfaToken
|
|
6212
|
+
}, {
|
|
6213
|
+
walletId: wallet.walletId,
|
|
6214
|
+
walletOperation
|
|
6178
6215
|
});
|
|
6179
6216
|
const dynamicKeyShares = data.keyShares.filter((keyShare)=>keyShare.encryptedAccountCredential !== null && keyShare.backupLocation === core.BackupLocation.DYNAMIC);
|
|
6180
6217
|
const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)=>// `decryptKeyShare` owns the `environmentId` fallback; passing
|
|
@@ -6842,8 +6879,10 @@ class DynamicWalletClient {
|
|
|
6842
6879
|
})) {
|
|
6843
6880
|
const walletData = this.getWalletFromMap(accountAddress);
|
|
6844
6881
|
const isPasswordEncrypted = isWalletPasswordEncrypted(walletData);
|
|
6845
|
-
//
|
|
6846
|
-
|
|
6882
|
+
// Locked wallet (password-encrypted, no password supplied): its shares need the
|
|
6883
|
+
// user's password, not the environmentId. Return it ENCRYPTED for any operation
|
|
6884
|
+
// rather than fall through to an envId-decrypt that throws KeyShareDecryptionError.
|
|
6885
|
+
if (isPasswordEncrypted && !password) {
|
|
6847
6886
|
// Skip fetch if encrypted shares are already cached in storage
|
|
6848
6887
|
const alreadyCached = await hasEncryptedSharesCached({
|
|
6849
6888
|
accountAddress,
|
|
@@ -6864,28 +6903,32 @@ class DynamicWalletClient {
|
|
|
6864
6903
|
}
|
|
6865
6904
|
return cachedWallet;
|
|
6866
6905
|
}
|
|
6906
|
+
// RECOVER on purpose: cache the full set a later unlockWallet needs, not the triggering op's subset.
|
|
6867
6907
|
const { shares } = this.recoverStrategy({
|
|
6868
6908
|
clientKeyShareBackupInfo: walletData.clientKeySharesBackupInfo,
|
|
6869
6909
|
thresholdSignatureScheme: walletData.thresholdSignatureScheme,
|
|
6870
6910
|
walletOperation: core.WalletOperation.RECOVER
|
|
6871
6911
|
});
|
|
6872
6912
|
const { dynamic: dynamicKeyShareIds } = shares;
|
|
6873
|
-
this.logger.info('[walletMap] getWallet:
|
|
6913
|
+
this.logger.info('[walletMap] getWallet: caching encrypted shares for locked wallet', {
|
|
6874
6914
|
context: {
|
|
6875
6915
|
accountAddress,
|
|
6876
6916
|
walletId: walletData.walletId,
|
|
6877
6917
|
externalKeyShareIds: dynamicKeyShareIds,
|
|
6878
6918
|
dynamicRequestId,
|
|
6879
|
-
isPasswordEncrypted
|
|
6919
|
+
isPasswordEncrypted,
|
|
6920
|
+
walletOperation
|
|
6880
6921
|
}
|
|
6881
6922
|
});
|
|
6882
|
-
const data = await this.
|
|
6923
|
+
const data = await this.recoverEncryptedSharesWithNonceRetry({
|
|
6883
6924
|
walletId: walletData.walletId,
|
|
6884
6925
|
shareSetId: walletData.shareSetId,
|
|
6885
6926
|
externalKeyShareIds: dynamicKeyShareIds,
|
|
6886
|
-
signedSessionId
|
|
6887
|
-
|
|
6888
|
-
|
|
6927
|
+
signedSessionId
|
|
6928
|
+
}, {
|
|
6929
|
+
walletId: walletData.walletId,
|
|
6930
|
+
context: 'getWallet:eagerLoad',
|
|
6931
|
+
dynamicRequestId
|
|
6889
6932
|
});
|
|
6890
6933
|
// Cache encrypted shares for later decryption by unlockWallet
|
|
6891
6934
|
const encryptedStorageKey = `${normalizeAddress(accountAddress)}${core.ENCRYPTED_SHARES_STORAGE_SUFFIX}`;
|
|
@@ -7022,13 +7065,14 @@ class DynamicWalletClient {
|
|
|
7022
7065
|
externalKeyShareIds
|
|
7023
7066
|
}
|
|
7024
7067
|
});
|
|
7025
|
-
const data = await this.
|
|
7068
|
+
const data = await this.recoverEncryptedSharesWithNonceRetry({
|
|
7026
7069
|
walletId: walletData.walletId,
|
|
7027
7070
|
externalKeyShareIds,
|
|
7028
7071
|
signedSessionId,
|
|
7029
|
-
mfaToken
|
|
7030
|
-
|
|
7031
|
-
|
|
7072
|
+
mfaToken
|
|
7073
|
+
}, {
|
|
7074
|
+
walletId: walletData.walletId,
|
|
7075
|
+
context: 'fetchAndCacheEncryptedShares'
|
|
7032
7076
|
});
|
|
7033
7077
|
if (!data) {
|
|
7034
7078
|
return null;
|
|
@@ -7476,6 +7520,13 @@ class DynamicWalletClient {
|
|
|
7476
7520
|
if (internalOptions == null ? void 0 : internalOptions.getSignedSessionId) {
|
|
7477
7521
|
this.getSignedSessionIdCallback = internalOptions.getSignedSessionId;
|
|
7478
7522
|
}
|
|
7523
|
+
// Read the callback lazily so it tracks `getSignedSessionIdCallback` even
|
|
7524
|
+
// when tests reassign it after construction.
|
|
7525
|
+
this.signedSession = new SignedSessionManager({
|
|
7526
|
+
logger: this.logger,
|
|
7527
|
+
sdkVersion: this.sdkVersion,
|
|
7528
|
+
getCallback: ()=>this.getSignedSessionIdCallback
|
|
7529
|
+
});
|
|
7479
7530
|
this.apiClient = new core.DynamicApiClient({
|
|
7480
7531
|
environmentId,
|
|
7481
7532
|
authToken,
|