@dynamic-labs-wallet/browser 1.0.1 → 1.0.3
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 +345 -82
- package/index.esm.js +345 -82
- package/package.json +3 -3
- package/src/backup/encryption/core.d.ts +16 -0
- package/src/backup/encryption/core.d.ts.map +1 -1
- package/src/backup/password/errors.d.ts.map +1 -1
- package/src/client.d.ts +4 -1
- package/src/client.d.ts.map +1 -1
- package/src/selectReshareDistribution.d.ts +11 -0
- package/src/selectReshareDistribution.d.ts.map +1 -0
- package/src/utils.d.ts.map +1 -1
package/index.esm.js
CHANGED
|
@@ -230,6 +230,25 @@ class InvalidPasswordError extends Error {
|
|
|
230
230
|
this.name = 'InvalidPasswordError';
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
+
const KEY_SHARE_DECRYPTION_ERROR = 'Decryption failed.';
|
|
234
|
+
/**
|
|
235
|
+
* Thrown when a key share cannot be decrypted but no user-supplied password was
|
|
236
|
+
* involved. Distinct from InvalidPasswordError, which is reserved for cases
|
|
237
|
+
* where a user-entered password fails to decrypt a password-encrypted wallet.
|
|
238
|
+
*
|
|
239
|
+
* Internal-only signal: the user-facing message is deliberately generic so it
|
|
240
|
+
* does not leak implementation detail (e.g. environmentId fallback,
|
|
241
|
+
* cross-environment cipher, KDF/version mismatch). The `name` and any attached
|
|
242
|
+
* `cause` carry the real diagnostic for logs / monitoring.
|
|
243
|
+
*/ class KeyShareDecryptionError extends Error {
|
|
244
|
+
constructor(options){
|
|
245
|
+
super(KEY_SHARE_DECRYPTION_ERROR);
|
|
246
|
+
this.name = 'KeyShareDecryptionError';
|
|
247
|
+
if ((options == null ? void 0 : options.cause) !== undefined) {
|
|
248
|
+
this.cause = options.cause;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
233
252
|
/**
|
|
234
253
|
* Check if an error is an OperationError from SubtleCrypto.decrypt,
|
|
235
254
|
* which indicates authentication failure (wrong password/derived key)
|
|
@@ -515,7 +534,15 @@ const writeCachedReason = (error, reason)=>{
|
|
|
515
534
|
// Frozen/sealed errors — ignore, just lose the cache on this path.
|
|
516
535
|
}
|
|
517
536
|
};
|
|
537
|
+
const isKeyShareDecryptionError = (error)=>error instanceof KeyShareDecryptionError || error instanceof Error && error.name === 'KeyShareDecryptionError';
|
|
518
538
|
const computeReason = (error)=>{
|
|
539
|
+
// KeyShareDecryptionError represents a system-side decryption failure where
|
|
540
|
+
// no user password was involved (e.g. envId-fallback failed on a
|
|
541
|
+
// non-password-encrypted wallet). Classify ahead of the InvalidPasswordError
|
|
542
|
+
// check so it doesn't get mislabeled as `wrong_password`.
|
|
543
|
+
if (isKeyShareDecryptionError(error)) {
|
|
544
|
+
return 'decryption_failure';
|
|
545
|
+
}
|
|
519
546
|
if (isPasswordMismatchError(error)) {
|
|
520
547
|
return 'wrong_password';
|
|
521
548
|
}
|
|
@@ -1054,6 +1081,7 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
|
|
|
1054
1081
|
if ((error == null ? void 0 : error.isRetryable) === false) return true;
|
|
1055
1082
|
if (isPasswordMismatchError(error)) return true;
|
|
1056
1083
|
if (error instanceof InvalidPasswordError) return true;
|
|
1084
|
+
if (error instanceof KeyShareDecryptionError) return true;
|
|
1057
1085
|
const message = error instanceof Error ? error.message : '';
|
|
1058
1086
|
if (!message) return false;
|
|
1059
1087
|
return NON_RETRYABLE_CEREMONY_ERROR_MESSAGES.some((msg)=>message.includes(msg));
|
|
@@ -2098,6 +2126,50 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
2098
2126
|
/** Track which wallets are currently executing inside a sign operation.
|
|
2099
2127
|
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
|
|
2100
2128
|
|
|
2129
|
+
// Pure routing helper: maps a completed reshare ceremony's results into the
|
|
2130
|
+
// ShareDistribution that storage/publish should follow. Lives outside the
|
|
2131
|
+
// client class so it's straightforward to reason about and unit-test
|
|
2132
|
+
// without spinning up DynamicWalletClient.
|
|
2133
|
+
const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders, existingReshareResults, newReshareResults })=>{
|
|
2134
|
+
const allClientShares = [
|
|
2135
|
+
...existingReshareResults,
|
|
2136
|
+
...newReshareResults
|
|
2137
|
+
];
|
|
2138
|
+
// Share-set delegation signal: same-parties reshare with zero new client
|
|
2139
|
+
// party. The refreshed rootUser-client output IS the delegated share —
|
|
2140
|
+
// it goes to the webhook out-of-band, NOT to Dynamic backup, and local
|
|
2141
|
+
// cache stays with gen1.
|
|
2142
|
+
if (resolvedDelegation && newReshareResults.length === 0 && existingReshareResults.length === 1) {
|
|
2143
|
+
return {
|
|
2144
|
+
clientShares: [],
|
|
2145
|
+
cloudProviderShares: {},
|
|
2146
|
+
delegatedShare: existingReshareResults[0]
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
if (resolvedDelegation && resolvedCloudProviders.length > 0) {
|
|
2150
|
+
return createDelegationWithCloudProviderDistribution({
|
|
2151
|
+
providers: resolvedCloudProviders,
|
|
2152
|
+
existingShares: existingReshareResults,
|
|
2153
|
+
delegatedShare: newReshareResults[0]
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
if (resolvedDelegation) {
|
|
2157
|
+
return createDelegationOnlyDistribution({
|
|
2158
|
+
existingShares: existingReshareResults,
|
|
2159
|
+
delegatedShare: newReshareResults[0]
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
if (resolvedCloudProviders.length > 0) {
|
|
2163
|
+
return createCloudProviderDistribution({
|
|
2164
|
+
providers: resolvedCloudProviders,
|
|
2165
|
+
allShares: allClientShares
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
return createDynamicOnlyDistribution({
|
|
2169
|
+
allShares: allClientShares
|
|
2170
|
+
});
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2101
2173
|
const ALG_LABEL_RSA = 'HYBRID-RSA-AES-256';
|
|
2102
2174
|
/**
|
|
2103
2175
|
* Convert base64 to base64url encoding
|
|
@@ -3487,11 +3559,19 @@ class DynamicWalletClient {
|
|
|
3487
3559
|
// Backup & activate server shares BEFORE saving to local storage.
|
|
3488
3560
|
// This prevents a mismatch where new client shares are stored locally
|
|
3489
3561
|
// but old server shares remain active if backup/activation fails.
|
|
3490
|
-
|
|
3562
|
+
// Rollback semantics — see runBackupWithRotationRollback.
|
|
3563
|
+
await this.runBackupWithRotationRollback({
|
|
3491
3564
|
accountAddress,
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3565
|
+
ceremony: 'refresh',
|
|
3566
|
+
sourceShareSetId: wallet.shareSetId,
|
|
3567
|
+
walletId: wallet.walletId,
|
|
3568
|
+
dynamicRequestId,
|
|
3569
|
+
runBackup: ()=>this.storeEncryptedBackupByWallet({
|
|
3570
|
+
accountAddress,
|
|
3571
|
+
clientKeyShares: refreshResults,
|
|
3572
|
+
password: password != null ? password : this.environmentId,
|
|
3573
|
+
signedSessionId
|
|
3574
|
+
})
|
|
3495
3575
|
});
|
|
3496
3576
|
await this.setClientKeySharesToStorage({
|
|
3497
3577
|
accountAddress,
|
|
@@ -3575,7 +3655,7 @@ class DynamicWalletClient {
|
|
|
3575
3655
|
* existingClientKeyShares: ClientKeyShare[]
|
|
3576
3656
|
* }>} Object containing new and existing client keygen results, IDs and shares
|
|
3577
3657
|
* @todo Support higher to lower reshare strategies
|
|
3578
|
-
*/ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme }) {
|
|
3658
|
+
*/ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, delegateToProjectEnvironment }) {
|
|
3579
3659
|
const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
|
|
3580
3660
|
const mpcSigner = getMPCSigner({
|
|
3581
3661
|
chainName,
|
|
@@ -3585,7 +3665,8 @@ class DynamicWalletClient {
|
|
|
3585
3665
|
// Determine share counts based on threshold signature schemes
|
|
3586
3666
|
const { newClientShareCount, existingClientShareCount } = getReshareConfig({
|
|
3587
3667
|
oldThresholdSignatureScheme,
|
|
3588
|
-
newThresholdSignatureScheme
|
|
3668
|
+
newThresholdSignatureScheme,
|
|
3669
|
+
delegateToProjectEnvironment
|
|
3589
3670
|
});
|
|
3590
3671
|
// Create new client shares
|
|
3591
3672
|
const newClientInitKeygenResults = await Promise.all(Array.from({
|
|
@@ -3661,16 +3742,50 @@ class DynamicWalletClient {
|
|
|
3661
3742
|
});
|
|
3662
3743
|
}
|
|
3663
3744
|
}
|
|
3664
|
-
//
|
|
3665
|
-
//
|
|
3666
|
-
//
|
|
3667
|
-
//
|
|
3668
|
-
//
|
|
3745
|
+
// Runs the post-ceremony backup-activation step (storeEncryptedBackupByWallet
|
|
3746
|
+
// for refresh, backupSharesWithDistribution for reshare). On failure, rolls
|
|
3747
|
+
// walletMap.shareSetId back to the source row.
|
|
3748
|
+
//
|
|
3749
|
+
// Why: ceremony_complete rotates walletMap.shareSetId forward to the new
|
|
3750
|
+
// pending row before this call, so the activation request carries the
|
|
3751
|
+
// correct id. If activation fails (server 500, network error, etc.), the
|
|
3752
|
+
// pending row stays `pending` on the server but walletMap is still pointing
|
|
3753
|
+
// at it locally — every subsequent sign / refresh sends the pending id and
|
|
3754
|
+
// the server rejects with 422 "Wallet is not active." Rolling the local
|
|
3755
|
+
// rotation back to `sourceShareSetId` (the still-active row at ceremony
|
|
3756
|
+
// start) keeps the wallet usable after a failed refresh / reshare.
|
|
3757
|
+
async runBackupWithRotationRollback({ accountAddress, ceremony, sourceShareSetId, walletId, dynamicRequestId, runBackup }) {
|
|
3758
|
+
try {
|
|
3759
|
+
await runBackup();
|
|
3760
|
+
} catch (backupError) {
|
|
3761
|
+
this.updateWalletMap(accountAddress, {
|
|
3762
|
+
shareSetId: sourceShareSetId
|
|
3763
|
+
});
|
|
3764
|
+
const tag = ceremony === 'refresh' ? '[WaasRefresh]' : '[WaasReshare]';
|
|
3765
|
+
this.logger.warn(`${tag} backup activation failed — rolled walletMap.shareSetId back to source`, {
|
|
3766
|
+
context: {
|
|
3767
|
+
walletId,
|
|
3768
|
+
restoredShareSetId: sourceShareSetId,
|
|
3769
|
+
dynamicRequestId
|
|
3770
|
+
}
|
|
3771
|
+
});
|
|
3772
|
+
throw backupError;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
// Persists the ceremony_complete payload into the wallet map.
|
|
3776
|
+
// - rootUser → rotate walletMap.shareSetId (if it actually changed).
|
|
3777
|
+
// - delegated → append/replace under otherShareSets[] via recordDelegatedShareSet
|
|
3778
|
+
// (requires newThresholdSignatureScheme passed in by the reshare caller).
|
|
3779
|
+
// Reshare delegation flow owns this branch; refresh hits the non-rootUser
|
|
3780
|
+
// skip path below because newThresholdSignatureScheme isn't threaded.
|
|
3781
|
+
// - server / undefined → no-op with a skip log so DataDog shows the path
|
|
3782
|
+
// (undefined is legacy redcoast that predates the share-set rollout —
|
|
3783
|
+
// server falls back to walletId resolution on the follow-up backup call).
|
|
3669
3784
|
//
|
|
3670
3785
|
// Each skip reason is logged distinctly so operators can grep DataDog for
|
|
3671
3786
|
// "legacy server callbacks" vs "delegation skips" vs "no-op same id"
|
|
3672
3787
|
// separately — three operationally different states.
|
|
3673
|
-
rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, extraContext }) {
|
|
3788
|
+
rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, newThresholdSignatureScheme, extraContext }) {
|
|
3674
3789
|
const baseContext = _extends({
|
|
3675
3790
|
walletId: wallet.walletId,
|
|
3676
3791
|
ceremony
|
|
@@ -3687,10 +3802,20 @@ class DynamicWalletClient {
|
|
|
3687
3802
|
});
|
|
3688
3803
|
return;
|
|
3689
3804
|
}
|
|
3805
|
+
if (shareSetType === 'delegated' && newThresholdSignatureScheme) {
|
|
3806
|
+
this.recordDelegatedShareSet({
|
|
3807
|
+
accountAddress,
|
|
3808
|
+
wallet,
|
|
3809
|
+
newShareSetId: shareSetId,
|
|
3810
|
+
newThresholdSignatureScheme
|
|
3811
|
+
});
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3690
3814
|
if (shareSetType !== 'rootUser') {
|
|
3691
|
-
// 'delegated'
|
|
3692
|
-
//
|
|
3693
|
-
//
|
|
3815
|
+
// 'delegated' (no newThresholdSignatureScheme, e.g. refresh) → SDK doesn't
|
|
3816
|
+
// own the otherShareSets[] write from refresh.
|
|
3817
|
+
// 'server' → internal server share set, not for SDK rotation.
|
|
3818
|
+
// undefined → server emitted partial payload (already warned upstream).
|
|
3694
3819
|
this.logger.info('[WaasShareSet] rotation skipped: non-rootUser share-set type', {
|
|
3695
3820
|
context: _extends({}, baseContext, {
|
|
3696
3821
|
shareSetType,
|
|
@@ -3717,6 +3842,38 @@ class DynamicWalletClient {
|
|
|
3717
3842
|
shareSetId
|
|
3718
3843
|
});
|
|
3719
3844
|
}
|
|
3845
|
+
// Records a new `delegated` share set under wallet.otherShareSets[],
|
|
3846
|
+
// replacing an existing delegated entry if one is already there (rather
|
|
3847
|
+
// than appending duplicates on retry). Only reachable from the reshare
|
|
3848
|
+
// delegation flow — refresh doesn't thread newThresholdSignatureScheme
|
|
3849
|
+
// so it falls through to the skip path before reaching here.
|
|
3850
|
+
recordDelegatedShareSet({ accountAddress, wallet, newShareSetId, newThresholdSignatureScheme }) {
|
|
3851
|
+
const walletProps = this.getWalletFromMap(accountAddress);
|
|
3852
|
+
var _walletProps_otherShareSets;
|
|
3853
|
+
const existing = (_walletProps_otherShareSets = walletProps == null ? void 0 : walletProps.otherShareSets) != null ? _walletProps_otherShareSets : [];
|
|
3854
|
+
const newEntry = {
|
|
3855
|
+
shareSetId: newShareSetId,
|
|
3856
|
+
shareSetType: 'delegated',
|
|
3857
|
+
thresholdSignatureScheme: newThresholdSignatureScheme,
|
|
3858
|
+
createdAt: new Date().toISOString()
|
|
3859
|
+
};
|
|
3860
|
+
const delegatedIdx = existing.findIndex((s)=>s.shareSetType === 'delegated');
|
|
3861
|
+
const updated = delegatedIdx >= 0 ? existing.map((s, i)=>i === delegatedIdx ? newEntry : s) : [
|
|
3862
|
+
...existing,
|
|
3863
|
+
newEntry
|
|
3864
|
+
];
|
|
3865
|
+
this.logger.info('[WaasShareSet] delegated shareSetId added by reshare ceremony', {
|
|
3866
|
+
context: {
|
|
3867
|
+
walletId: wallet.walletId,
|
|
3868
|
+
rootUserShareSetId: wallet.shareSetId,
|
|
3869
|
+
delegatedShareSetId: newShareSetId,
|
|
3870
|
+
replacedExistingDelegated: delegatedIdx >= 0
|
|
3871
|
+
}
|
|
3872
|
+
});
|
|
3873
|
+
this.updateWalletMap(accountAddress, {
|
|
3874
|
+
otherShareSets: updated
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3720
3877
|
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false, googleDriveAccessToken }) {
|
|
3721
3878
|
const dynamicRequestId = v4();
|
|
3722
3879
|
// Password validation - wrapped in try-catch for consistent error handling
|
|
@@ -3759,7 +3916,8 @@ class DynamicWalletClient {
|
|
|
3759
3916
|
try {
|
|
3760
3917
|
const { existingClientShareCount } = getReshareConfig({
|
|
3761
3918
|
oldThresholdSignatureScheme,
|
|
3762
|
-
newThresholdSignatureScheme
|
|
3919
|
+
newThresholdSignatureScheme,
|
|
3920
|
+
delegateToProjectEnvironment: resolvedDelegation
|
|
3763
3921
|
});
|
|
3764
3922
|
const wallet = await this.getWallet({
|
|
3765
3923
|
accountAddress,
|
|
@@ -3773,11 +3931,14 @@ class DynamicWalletClient {
|
|
|
3773
3931
|
accountAddress,
|
|
3774
3932
|
wallet,
|
|
3775
3933
|
oldThresholdSignatureScheme,
|
|
3776
|
-
newThresholdSignatureScheme
|
|
3934
|
+
newThresholdSignatureScheme,
|
|
3935
|
+
delegateToProjectEnvironment: resolvedDelegation
|
|
3777
3936
|
});
|
|
3778
|
-
// Ensure existing client key shares
|
|
3779
|
-
|
|
3780
|
-
|
|
3937
|
+
// Ensure existing client key shares match what getReshareConfig expected.
|
|
3938
|
+
// For most flows this is >= 1; for 2/2 → 2/2 + delegation it's 0 (the
|
|
3939
|
+
// ceremony has no surviving client-side party from the old quorum).
|
|
3940
|
+
if (existingClientKeyShares.length < existingClientShareCount) {
|
|
3941
|
+
throw new Error(`Expected ${existingClientShareCount} existing client key share(s) for reshare operation but only found ${existingClientKeyShares.length} for account address: ${accountAddress}`);
|
|
3781
3942
|
}
|
|
3782
3943
|
const clientKeygenIds = [
|
|
3783
3944
|
...newClientKeygenIds,
|
|
@@ -3841,6 +4002,7 @@ class DynamicWalletClient {
|
|
|
3841
4002
|
ceremony: 'reshare',
|
|
3842
4003
|
shareSetId,
|
|
3843
4004
|
shareSetType,
|
|
4005
|
+
newThresholdSignatureScheme,
|
|
3844
4006
|
extraContext: {
|
|
3845
4007
|
oldThresholdSignatureScheme,
|
|
3846
4008
|
newThresholdSignatureScheme,
|
|
@@ -3964,41 +4126,12 @@ class DynamicWalletClient {
|
|
|
3964
4126
|
}
|
|
3965
4127
|
throw reshareError;
|
|
3966
4128
|
}
|
|
3967
|
-
const
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
// Use effective* variables which may have been populated from existing wallet state
|
|
3974
|
-
if (resolvedDelegation && resolvedCloudProviders.length > 0) {
|
|
3975
|
-
// Delegation + Cloud Providers: Client's existing share backs up to both Dynamic and cloud providers.
|
|
3976
|
-
// The new share goes to the webhook for delegation.
|
|
3977
|
-
distribution = createDelegationWithCloudProviderDistribution({
|
|
3978
|
-
providers: resolvedCloudProviders,
|
|
3979
|
-
existingShares: existingReshareResults,
|
|
3980
|
-
delegatedShare: newReshareResults[0]
|
|
3981
|
-
});
|
|
3982
|
-
} else if (resolvedDelegation) {
|
|
3983
|
-
// Delegation only: Client's existing share backs up to Dynamic.
|
|
3984
|
-
// The new share goes to the webhook for delegation. No cloud provider backup.
|
|
3985
|
-
distribution = createDelegationOnlyDistribution({
|
|
3986
|
-
existingShares: existingReshareResults,
|
|
3987
|
-
delegatedShare: newReshareResults[0]
|
|
3988
|
-
});
|
|
3989
|
-
} else if (resolvedCloudProviders.length > 0) {
|
|
3990
|
-
// Cloud Providers only: Split shares between Dynamic (N-1) and cloud providers (1).
|
|
3991
|
-
// The last share (new share) goes to cloud providers.
|
|
3992
|
-
distribution = createCloudProviderDistribution({
|
|
3993
|
-
providers: resolvedCloudProviders,
|
|
3994
|
-
allShares: allClientShares
|
|
3995
|
-
});
|
|
3996
|
-
} else {
|
|
3997
|
-
// No delegation, no cloud providers: All shares go to Dynamic backend only.
|
|
3998
|
-
distribution = createDynamicOnlyDistribution({
|
|
3999
|
-
allShares: allClientShares
|
|
4000
|
-
});
|
|
4001
|
-
}
|
|
4129
|
+
const distribution = selectReshareDistribution({
|
|
4130
|
+
resolvedDelegation,
|
|
4131
|
+
resolvedCloudProviders,
|
|
4132
|
+
existingReshareResults,
|
|
4133
|
+
newReshareResults
|
|
4134
|
+
});
|
|
4002
4135
|
this.updateWalletMap(accountAddress, {
|
|
4003
4136
|
thresholdSignatureScheme: newThresholdSignatureScheme
|
|
4004
4137
|
});
|
|
@@ -4040,19 +4173,39 @@ class DynamicWalletClient {
|
|
|
4040
4173
|
// Backup & activate server shares BEFORE saving to local storage.
|
|
4041
4174
|
// This prevents a mismatch where new client shares are stored locally
|
|
4042
4175
|
// but old server shares remain active if backup/activation fails.
|
|
4043
|
-
|
|
4176
|
+
// Rollback semantics — see runBackupWithRotationRollback.
|
|
4177
|
+
await this.runBackupWithRotationRollback({
|
|
4044
4178
|
accountAddress,
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4179
|
+
ceremony: 'reshare',
|
|
4180
|
+
sourceShareSetId: wallet.shareSetId,
|
|
4181
|
+
walletId: wallet.walletId,
|
|
4182
|
+
dynamicRequestId,
|
|
4183
|
+
runBackup: ()=>this.backupSharesWithDistribution({
|
|
4184
|
+
accountAddress,
|
|
4185
|
+
password,
|
|
4186
|
+
signedSessionId,
|
|
4187
|
+
distribution,
|
|
4188
|
+
googleDriveAccessToken
|
|
4189
|
+
})
|
|
4049
4190
|
});
|
|
4050
4191
|
// Store client key shares to storage (localStorage or secureStorage)
|
|
4051
4192
|
// only after backup succeeds.
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4193
|
+
//
|
|
4194
|
+
// Share-set delegation (FF=on) skips this entirely — the ceremony
|
|
4195
|
+
// output's refreshed rootUser-client share went to the webhook
|
|
4196
|
+
// (delegatedShare), and the original gen1 rootUser-client share must
|
|
4197
|
+
// stay in local cache so the user can still sign rootUser. See the
|
|
4198
|
+
// mixed-share-sign isolation property tested in
|
|
4199
|
+
// `wallet-service/scripts/twoOfTwoReshareExperiment.ts`. Calling this
|
|
4200
|
+
// with `clientShares: []` would clear the local cache (per the
|
|
4201
|
+
// method's "full replacement" semantics), breaking signing until
|
|
4202
|
+
// recovery refetches from Dynamic.
|
|
4203
|
+
if (distribution.clientShares.length > 0) {
|
|
4204
|
+
await this.setClientKeySharesToStorage({
|
|
4205
|
+
accountAddress,
|
|
4206
|
+
clientKeyShares: distribution.clientShares
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
4056
4209
|
// Operation summary — correlates against downstream sign failures /
|
|
4057
4210
|
// pending-rotation reports. `ceremonyCallbackFired: false` here means
|
|
4058
4211
|
// either the server is legacy (didn't emit ceremony_complete) or the
|
|
@@ -4094,7 +4247,7 @@ class DynamicWalletClient {
|
|
|
4094
4247
|
throw error;
|
|
4095
4248
|
}
|
|
4096
4249
|
}
|
|
4097
|
-
async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, operationName }) {
|
|
4250
|
+
async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
|
|
4098
4251
|
try {
|
|
4099
4252
|
const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
|
|
4100
4253
|
if (!delegateToProjectEnvironment) {
|
|
@@ -4111,8 +4264,11 @@ class DynamicWalletClient {
|
|
|
4111
4264
|
}
|
|
4112
4265
|
const walletData = this.getWalletFromMap(accountAddress);
|
|
4113
4266
|
const currentThresholdSignatureScheme = walletData.thresholdSignatureScheme;
|
|
4114
|
-
//
|
|
4115
|
-
|
|
4267
|
+
// Under the share-set reshare flow, rootUser is not mutated — the
|
|
4268
|
+
// delegation creates a separate `delegated` share set. So we don't
|
|
4269
|
+
// re-pin existing cloud providers; preservation is irrelevant when the
|
|
4270
|
+
// primary share set is untouched.
|
|
4271
|
+
const activeProviders = useShareSetReshare ? [] : getActiveCloudProviders(walletData == null ? void 0 : walletData.clientKeySharesBackupInfo);
|
|
4116
4272
|
await this.reshare({
|
|
4117
4273
|
chainName: walletData.chainName,
|
|
4118
4274
|
accountAddress,
|
|
@@ -4137,19 +4293,82 @@ class DynamicWalletClient {
|
|
|
4137
4293
|
}
|
|
4138
4294
|
}
|
|
4139
4295
|
async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
4296
|
+
var _this_featureFlags;
|
|
4297
|
+
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4298
|
+
if (useShareSetReshare) {
|
|
4299
|
+
var _this_getWalletFromMap;
|
|
4300
|
+
// FF on: delegation creates a separate `delegated` share set without
|
|
4301
|
+
// touching rootUser. If the wallet already has one, this is a no-op.
|
|
4302
|
+
const existingDelegated = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
|
|
4303
|
+
if (existingDelegated) {
|
|
4304
|
+
var _this_getWalletFromMap1;
|
|
4305
|
+
this.logger.info('[WaasShareSet] delegateKeyShares no-op — wallet already has a delegated share set', {
|
|
4306
|
+
accountAddress,
|
|
4307
|
+
delegatedShareSetId: existingDelegated.shareSetId
|
|
4308
|
+
});
|
|
4309
|
+
const backupInfo = (_this_getWalletFromMap1 = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap1.clientKeySharesBackupInfo;
|
|
4310
|
+
return (backupInfo == null ? void 0 : backupInfo.backups[BackupLocation.DELEGATED]) || [];
|
|
4311
|
+
}
|
|
4312
|
+
this.logger.info('[WaasShareSet] delegateKeyShares using share-set reshare path', {
|
|
4313
|
+
accountAddress
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4140
4316
|
await this.performDelegationOperation({
|
|
4141
4317
|
accountAddress,
|
|
4142
4318
|
password,
|
|
4143
4319
|
signedSessionId,
|
|
4144
4320
|
mfaToken,
|
|
4145
|
-
|
|
4146
|
-
|
|
4321
|
+
// FF on: reshare to 2/2 — the new delegated share set is always 2/2
|
|
4322
|
+
// (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
|
|
4323
|
+
// + delegation case that returns newClientShareCount=1 (the share
|
|
4324
|
+
// routed to the webhook) so the ceremony generates it correctly.
|
|
4325
|
+
// FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
|
|
4326
|
+
newThresholdSignatureScheme: useShareSetReshare ? ThresholdSignatureScheme.TWO_OF_TWO : ThresholdSignatureScheme.TWO_OF_THREE,
|
|
4327
|
+
operationName: 'delegateKeyShares',
|
|
4328
|
+
useShareSetReshare
|
|
4147
4329
|
});
|
|
4148
4330
|
const backupInfo = this.getWalletFromMap(accountAddress).clientKeySharesBackupInfo;
|
|
4149
4331
|
const delegatedKeyShares = backupInfo.backups[BackupLocation.DELEGATED] || [];
|
|
4150
4332
|
return delegatedKeyShares;
|
|
4151
4333
|
}
|
|
4152
4334
|
async revokeDelegation({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
4335
|
+
var _this_featureFlags;
|
|
4336
|
+
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4337
|
+
if (useShareSetReshare) {
|
|
4338
|
+
var _this_getWalletFromMap;
|
|
4339
|
+
const wallet = await this.getWallet({
|
|
4340
|
+
accountAddress,
|
|
4341
|
+
walletOperation: WalletOperation.NO_OPERATION,
|
|
4342
|
+
password,
|
|
4343
|
+
signedSessionId
|
|
4344
|
+
});
|
|
4345
|
+
const delegatedShareSet = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
|
|
4346
|
+
if (!delegatedShareSet) {
|
|
4347
|
+
this.logger.info('[WaasShareSet] revokeDelegation called but no delegated share set found', {
|
|
4348
|
+
accountAddress
|
|
4349
|
+
});
|
|
4350
|
+
return;
|
|
4351
|
+
}
|
|
4352
|
+
this.logger.info('[WaasShareSet] revokeDelegation via share-set REST DELETE (no MPC ceremony)', {
|
|
4353
|
+
accountAddress,
|
|
4354
|
+
delegatedShareSetId: delegatedShareSet.shareSetId,
|
|
4355
|
+
walletId: wallet.walletId
|
|
4356
|
+
});
|
|
4357
|
+
await this.apiClient.revokeDelegation({
|
|
4358
|
+
walletId: wallet.walletId,
|
|
4359
|
+
shareSetId: delegatedShareSet.shareSetId,
|
|
4360
|
+
signedSessionId,
|
|
4361
|
+
dynamicRequestId: v4().replaceAll('-', '')
|
|
4362
|
+
});
|
|
4363
|
+
// Refresh wallet state so otherShareSets / backups reflect the cleared delegation
|
|
4364
|
+
await this.getWallet({
|
|
4365
|
+
accountAddress,
|
|
4366
|
+
walletOperation: WalletOperation.NO_OPERATION,
|
|
4367
|
+
password,
|
|
4368
|
+
signedSessionId
|
|
4369
|
+
});
|
|
4370
|
+
return;
|
|
4371
|
+
}
|
|
4153
4372
|
await this.performDelegationOperation({
|
|
4154
4373
|
accountAddress,
|
|
4155
4374
|
password,
|
|
@@ -4696,7 +4915,7 @@ class DynamicWalletClient {
|
|
|
4696
4915
|
hasDelegatedShare: !!distribution.delegatedShare
|
|
4697
4916
|
}));
|
|
4698
4917
|
try {
|
|
4699
|
-
var _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
4918
|
+
var _this_getWalletFromMap, _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
4700
4919
|
// `let` so the retry path can swap in a freshly-signed session via the reverse channel on 400.
|
|
4701
4920
|
let resolvedSignedSessionId = await this.resolveSignedSessionId(signedSessionId);
|
|
4702
4921
|
const canRefreshSignedSessionId = this.getSignedSessionIdCallback !== undefined;
|
|
@@ -4791,10 +5010,24 @@ class DynamicWalletClient {
|
|
|
4791
5010
|
googleDriveAccessToken
|
|
4792
5011
|
}));
|
|
4793
5012
|
}
|
|
5013
|
+
// When this ceremony minted a separate `delegated` share set (FF=on
|
|
5014
|
+
// path), backup activation must target that share set — not rootUser —
|
|
5015
|
+
// so `/delegatedAccess/delivery` and `/backup/locations` resolve to the
|
|
5016
|
+
// pending delegated row instead of mutating rootUser. The FF=off path
|
|
5017
|
+
// leaves `otherShareSets` empty and falls back to rootUser, matching
|
|
5018
|
+
// legacy in-place delegation behavior.
|
|
5019
|
+
//
|
|
5020
|
+
// Read otherShareSets fresh from the wallet map — `recordDelegatedShareSet`
|
|
5021
|
+
// wrote the new entry during `ceremony_complete`, which fired between
|
|
5022
|
+
// when `walletData` was captured at the top of this method and now.
|
|
5023
|
+
const freshOtherShareSets = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets;
|
|
5024
|
+
const delegatedShareSet = distribution.delegatedShare ? getDelegatedShareSet(freshOtherShareSets) : undefined;
|
|
5025
|
+
var _delegatedShareSet_shareSetId;
|
|
5026
|
+
const targetShareSetId = (_delegatedShareSet_shareSetId = delegatedShareSet == null ? void 0 : delegatedShareSet.shareSetId) != null ? _delegatedShareSet_shareSetId : walletData.shareSetId;
|
|
4794
5027
|
if (distribution.delegatedShare) {
|
|
4795
5028
|
uploadPromises.push(retryPromise(()=>this.publishDelegatedShare({
|
|
4796
5029
|
walletId: walletData.walletId,
|
|
4797
|
-
shareSetId:
|
|
5030
|
+
shareSetId: targetShareSetId,
|
|
4798
5031
|
delegatedShare: distribution.delegatedShare,
|
|
4799
5032
|
signedSessionId: resolvedSignedSessionId,
|
|
4800
5033
|
dynamicRequestId,
|
|
@@ -4824,7 +5057,10 @@ class DynamicWalletClient {
|
|
|
4824
5057
|
// won't fix.
|
|
4825
5058
|
const backupData = await retryPromise(()=>this.apiClient.markKeySharesAsBackedUp({
|
|
4826
5059
|
walletId: walletData.walletId,
|
|
4827
|
-
|
|
5060
|
+
// Same routing rule as publishDelegatedShare above: when this
|
|
5061
|
+
// ceremony minted a separate `delegated` share set, the atomic
|
|
5062
|
+
// swap in /backup/locations must activate THAT row, not rootUser.
|
|
5063
|
+
shareSetId: targetShareSetId,
|
|
4828
5064
|
locations,
|
|
4829
5065
|
dynamicRequestId,
|
|
4830
5066
|
passwordUpdateBatchId
|
|
@@ -5222,13 +5458,35 @@ class DynamicWalletClient {
|
|
|
5222
5458
|
}
|
|
5223
5459
|
async decryptKeyShare({ keyShare, password }) {
|
|
5224
5460
|
const decodedKeyShare = JSON.parse(Buffer.from(keyShare, 'base64').toString());
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
const
|
|
5231
|
-
|
|
5461
|
+
// Track whether a user-supplied password was provided so we can emit a
|
|
5462
|
+
// distinct error class on failure. The default `environmentId` fallback is
|
|
5463
|
+
// applied here (and only here) so the original `password` argument retains
|
|
5464
|
+
// the caller's intent — see `recoverEncryptedBackupByWallet` / `getWallet`,
|
|
5465
|
+
// which no longer apply their own fallback.
|
|
5466
|
+
const usedDefaultPassword = !password;
|
|
5467
|
+
const effectivePassword = password != null ? password : this.environmentId;
|
|
5468
|
+
try {
|
|
5469
|
+
const decryptedKeyShare = await decryptData({
|
|
5470
|
+
data: decodedKeyShare,
|
|
5471
|
+
password: effectivePassword
|
|
5472
|
+
});
|
|
5473
|
+
this.logPasswordSharePresence(password, 'decrypt');
|
|
5474
|
+
const deserializedKeyShare = JSON.parse(decryptedKeyShare);
|
|
5475
|
+
return deserializedKeyShare;
|
|
5476
|
+
} catch (error) {
|
|
5477
|
+
// When no user password was supplied, an InvalidPasswordError is
|
|
5478
|
+
// misleading — the user never entered a password, so the "wrong password"
|
|
5479
|
+
// framing doesn't apply. Re-throw as a distinct system-side error so
|
|
5480
|
+
// dashboards/logs classify it as `decryption_failure` instead of
|
|
5481
|
+
// `wrong_password`, and surface that the cause is most likely an
|
|
5482
|
+
// environment-ID mismatch (e.g. wallet encrypted in another environment).
|
|
5483
|
+
if (usedDefaultPassword && error instanceof InvalidPasswordError) {
|
|
5484
|
+
throw new KeyShareDecryptionError({
|
|
5485
|
+
cause: error
|
|
5486
|
+
});
|
|
5487
|
+
}
|
|
5488
|
+
throw error;
|
|
5489
|
+
}
|
|
5232
5490
|
}
|
|
5233
5491
|
/**
|
|
5234
5492
|
* Validates that the provided password is consistent with existing encrypted wallets.
|
|
@@ -5484,9 +5742,12 @@ class DynamicWalletClient {
|
|
|
5484
5742
|
userId: this.userId
|
|
5485
5743
|
});
|
|
5486
5744
|
const dynamicKeyShares = data.keyShares.filter((keyShare)=>keyShare.encryptedAccountCredential !== null && keyShare.backupLocation === BackupLocation.DYNAMIC);
|
|
5487
|
-
const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)
|
|
5745
|
+
const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)=>// `decryptKeyShare` owns the `environmentId` fallback; passing
|
|
5746
|
+
// `password` (possibly undefined) lets it distinguish a user-supplied
|
|
5747
|
+
// password failure from an envId-fallback failure.
|
|
5748
|
+
this.decryptKeyShare({
|
|
5488
5749
|
keyShare: keyShare.encryptedAccountCredential,
|
|
5489
|
-
password
|
|
5750
|
+
password
|
|
5490
5751
|
})));
|
|
5491
5752
|
if (storeRecoveredShares) {
|
|
5492
5753
|
await this.setClientKeySharesToStorage({
|
|
@@ -6187,9 +6448,11 @@ class DynamicWalletClient {
|
|
|
6187
6448
|
return wallet;
|
|
6188
6449
|
}
|
|
6189
6450
|
// TODO(zfaizal2): throw error if signedSessionId is not provided after service deploy
|
|
6451
|
+
// `decryptKeyShare` owns the environmentId fallback so the original
|
|
6452
|
+
// `password` (possibly undefined) propagates here.
|
|
6190
6453
|
const decryptedKeyShares = await this.recoverEncryptedBackupByWallet({
|
|
6191
6454
|
accountAddress,
|
|
6192
|
-
password
|
|
6455
|
+
password,
|
|
6193
6456
|
walletOperation: walletOperation,
|
|
6194
6457
|
signedSessionId,
|
|
6195
6458
|
shareCount
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dynamic-labs-wallet/browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"license": "Licensed under the Dynamic Labs, Inc. Terms Of Service (https://www.dynamic.xyz/terms-conditions)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@dynamic-labs-wallet/core": "1.0.
|
|
7
|
+
"@dynamic-labs-wallet/core": "1.0.3",
|
|
8
8
|
"@dynamic-labs-wallet/forward-mpc-client": "0.10.0",
|
|
9
|
-
"@dynamic-labs-wallet/primitives": "1.0.
|
|
9
|
+
"@dynamic-labs-wallet/primitives": "1.0.3",
|
|
10
10
|
"@dynamic-labs/sdk-api-core": "^0.0.984",
|
|
11
11
|
"argon2id": "1.0.1",
|
|
12
12
|
"axios": "1.15.2",
|
|
@@ -4,6 +4,22 @@ export declare const INVALID_PASSWORD_ERROR = "Decryption failed: Invalid passwo
|
|
|
4
4
|
export declare class InvalidPasswordError extends Error {
|
|
5
5
|
constructor();
|
|
6
6
|
}
|
|
7
|
+
export declare const KEY_SHARE_DECRYPTION_ERROR = "Decryption failed.";
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when a key share cannot be decrypted but no user-supplied password was
|
|
10
|
+
* involved. Distinct from InvalidPasswordError, which is reserved for cases
|
|
11
|
+
* where a user-entered password fails to decrypt a password-encrypted wallet.
|
|
12
|
+
*
|
|
13
|
+
* Internal-only signal: the user-facing message is deliberately generic so it
|
|
14
|
+
* does not leak implementation detail (e.g. environmentId fallback,
|
|
15
|
+
* cross-environment cipher, KDF/version mismatch). The `name` and any attached
|
|
16
|
+
* `cause` carry the real diagnostic for logs / monitoring.
|
|
17
|
+
*/
|
|
18
|
+
export declare class KeyShareDecryptionError extends Error {
|
|
19
|
+
constructor(options?: {
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
7
23
|
/**
|
|
8
24
|
* Encrypts data using the specified encryption version.
|
|
9
25
|
* Always uses the latest encryption configuration for new encryptions by default.
|