@dynamic-labs-wallet/browser 1.0.2 → 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 +290 -73
- package/index.esm.js +290 -73
- 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 +3 -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.cjs
CHANGED
|
@@ -229,6 +229,25 @@ class InvalidPasswordError extends Error {
|
|
|
229
229
|
this.name = 'InvalidPasswordError';
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
+
const KEY_SHARE_DECRYPTION_ERROR = 'Decryption failed.';
|
|
233
|
+
/**
|
|
234
|
+
* Thrown when a key share cannot be decrypted but no user-supplied password was
|
|
235
|
+
* involved. Distinct from InvalidPasswordError, which is reserved for cases
|
|
236
|
+
* where a user-entered password fails to decrypt a password-encrypted wallet.
|
|
237
|
+
*
|
|
238
|
+
* Internal-only signal: the user-facing message is deliberately generic so it
|
|
239
|
+
* does not leak implementation detail (e.g. environmentId fallback,
|
|
240
|
+
* cross-environment cipher, KDF/version mismatch). The `name` and any attached
|
|
241
|
+
* `cause` carry the real diagnostic for logs / monitoring.
|
|
242
|
+
*/ class KeyShareDecryptionError extends Error {
|
|
243
|
+
constructor(options){
|
|
244
|
+
super(KEY_SHARE_DECRYPTION_ERROR);
|
|
245
|
+
this.name = 'KeyShareDecryptionError';
|
|
246
|
+
if ((options == null ? void 0 : options.cause) !== undefined) {
|
|
247
|
+
this.cause = options.cause;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
232
251
|
/**
|
|
233
252
|
* Check if an error is an OperationError from SubtleCrypto.decrypt,
|
|
234
253
|
* which indicates authentication failure (wrong password/derived key)
|
|
@@ -514,7 +533,15 @@ const writeCachedReason = (error, reason)=>{
|
|
|
514
533
|
// Frozen/sealed errors — ignore, just lose the cache on this path.
|
|
515
534
|
}
|
|
516
535
|
};
|
|
536
|
+
const isKeyShareDecryptionError = (error)=>error instanceof KeyShareDecryptionError || error instanceof Error && error.name === 'KeyShareDecryptionError';
|
|
517
537
|
const computeReason = (error)=>{
|
|
538
|
+
// KeyShareDecryptionError represents a system-side decryption failure where
|
|
539
|
+
// no user password was involved (e.g. envId-fallback failed on a
|
|
540
|
+
// non-password-encrypted wallet). Classify ahead of the InvalidPasswordError
|
|
541
|
+
// check so it doesn't get mislabeled as `wrong_password`.
|
|
542
|
+
if (isKeyShareDecryptionError(error)) {
|
|
543
|
+
return 'decryption_failure';
|
|
544
|
+
}
|
|
518
545
|
if (isPasswordMismatchError(error)) {
|
|
519
546
|
return 'wrong_password';
|
|
520
547
|
}
|
|
@@ -1053,6 +1080,7 @@ const logRetryExhausted = (operationName, maxAttempts, errorContext, logContext)
|
|
|
1053
1080
|
if ((error == null ? void 0 : error.isRetryable) === false) return true;
|
|
1054
1081
|
if (isPasswordMismatchError(error)) return true;
|
|
1055
1082
|
if (error instanceof InvalidPasswordError) return true;
|
|
1083
|
+
if (error instanceof KeyShareDecryptionError) return true;
|
|
1056
1084
|
const message = error instanceof Error ? error.message : '';
|
|
1057
1085
|
if (!message) return false;
|
|
1058
1086
|
return NON_RETRYABLE_CEREMONY_ERROR_MESSAGES.some((msg)=>message.includes(msg));
|
|
@@ -2097,6 +2125,50 @@ const createDynamicOnlyDistribution = ({ allShares })=>({
|
|
|
2097
2125
|
/** Track which wallets are currently executing inside a sign operation.
|
|
2098
2126
|
* Used to prevent deadlocks when recovery is called from within a sign op. */ WalletQueueManager.walletsWithActiveSignOp = new Map();
|
|
2099
2127
|
|
|
2128
|
+
// Pure routing helper: maps a completed reshare ceremony's results into the
|
|
2129
|
+
// ShareDistribution that storage/publish should follow. Lives outside the
|
|
2130
|
+
// client class so it's straightforward to reason about and unit-test
|
|
2131
|
+
// without spinning up DynamicWalletClient.
|
|
2132
|
+
const selectReshareDistribution = ({ resolvedDelegation, resolvedCloudProviders, existingReshareResults, newReshareResults })=>{
|
|
2133
|
+
const allClientShares = [
|
|
2134
|
+
...existingReshareResults,
|
|
2135
|
+
...newReshareResults
|
|
2136
|
+
];
|
|
2137
|
+
// Share-set delegation signal: same-parties reshare with zero new client
|
|
2138
|
+
// party. The refreshed rootUser-client output IS the delegated share —
|
|
2139
|
+
// it goes to the webhook out-of-band, NOT to Dynamic backup, and local
|
|
2140
|
+
// cache stays with gen1.
|
|
2141
|
+
if (resolvedDelegation && newReshareResults.length === 0 && existingReshareResults.length === 1) {
|
|
2142
|
+
return {
|
|
2143
|
+
clientShares: [],
|
|
2144
|
+
cloudProviderShares: {},
|
|
2145
|
+
delegatedShare: existingReshareResults[0]
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
if (resolvedDelegation && resolvedCloudProviders.length > 0) {
|
|
2149
|
+
return createDelegationWithCloudProviderDistribution({
|
|
2150
|
+
providers: resolvedCloudProviders,
|
|
2151
|
+
existingShares: existingReshareResults,
|
|
2152
|
+
delegatedShare: newReshareResults[0]
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
if (resolvedDelegation) {
|
|
2156
|
+
return createDelegationOnlyDistribution({
|
|
2157
|
+
existingShares: existingReshareResults,
|
|
2158
|
+
delegatedShare: newReshareResults[0]
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
if (resolvedCloudProviders.length > 0) {
|
|
2162
|
+
return createCloudProviderDistribution({
|
|
2163
|
+
providers: resolvedCloudProviders,
|
|
2164
|
+
allShares: allClientShares
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
return createDynamicOnlyDistribution({
|
|
2168
|
+
allShares: allClientShares
|
|
2169
|
+
});
|
|
2170
|
+
};
|
|
2171
|
+
|
|
2100
2172
|
const ALG_LABEL_RSA = 'HYBRID-RSA-AES-256';
|
|
2101
2173
|
/**
|
|
2102
2174
|
* Convert base64 to base64url encoding
|
|
@@ -3582,7 +3654,7 @@ class DynamicWalletClient {
|
|
|
3582
3654
|
* existingClientKeyShares: ClientKeyShare[]
|
|
3583
3655
|
* }>} Object containing new and existing client keygen results, IDs and shares
|
|
3584
3656
|
* @todo Support higher to lower reshare strategies
|
|
3585
|
-
*/ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme }) {
|
|
3657
|
+
*/ async reshareStrategy({ chainName, wallet, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, delegateToProjectEnvironment }) {
|
|
3586
3658
|
const bitcoinConfig = this.getBitcoinConfigForChain(chainName, accountAddress);
|
|
3587
3659
|
const mpcSigner = getMPCSigner({
|
|
3588
3660
|
chainName,
|
|
@@ -3592,7 +3664,8 @@ class DynamicWalletClient {
|
|
|
3592
3664
|
// Determine share counts based on threshold signature schemes
|
|
3593
3665
|
const { newClientShareCount, existingClientShareCount } = core.getReshareConfig({
|
|
3594
3666
|
oldThresholdSignatureScheme,
|
|
3595
|
-
newThresholdSignatureScheme
|
|
3667
|
+
newThresholdSignatureScheme,
|
|
3668
|
+
delegateToProjectEnvironment
|
|
3596
3669
|
});
|
|
3597
3670
|
// Create new client shares
|
|
3598
3671
|
const newClientInitKeygenResults = await Promise.all(Array.from({
|
|
@@ -3698,16 +3771,20 @@ class DynamicWalletClient {
|
|
|
3698
3771
|
throw backupError;
|
|
3699
3772
|
}
|
|
3700
3773
|
}
|
|
3701
|
-
//
|
|
3702
|
-
//
|
|
3703
|
-
//
|
|
3704
|
-
//
|
|
3705
|
-
//
|
|
3774
|
+
// Persists the ceremony_complete payload into the wallet map.
|
|
3775
|
+
// - rootUser → rotate walletMap.shareSetId (if it actually changed).
|
|
3776
|
+
// - delegated → append/replace under otherShareSets[] via recordDelegatedShareSet
|
|
3777
|
+
// (requires newThresholdSignatureScheme passed in by the reshare caller).
|
|
3778
|
+
// Reshare delegation flow owns this branch; refresh hits the non-rootUser
|
|
3779
|
+
// skip path below because newThresholdSignatureScheme isn't threaded.
|
|
3780
|
+
// - server / undefined → no-op with a skip log so DataDog shows the path
|
|
3781
|
+
// (undefined is legacy redcoast that predates the share-set rollout —
|
|
3782
|
+
// server falls back to walletId resolution on the follow-up backup call).
|
|
3706
3783
|
//
|
|
3707
3784
|
// Each skip reason is logged distinctly so operators can grep DataDog for
|
|
3708
3785
|
// "legacy server callbacks" vs "delegation skips" vs "no-op same id"
|
|
3709
3786
|
// separately — three operationally different states.
|
|
3710
|
-
rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, extraContext }) {
|
|
3787
|
+
rotateRootUserShareSetIdIfChanged({ accountAddress, wallet, ceremony, shareSetId, shareSetType, newThresholdSignatureScheme, extraContext }) {
|
|
3711
3788
|
const baseContext = _extends({
|
|
3712
3789
|
walletId: wallet.walletId,
|
|
3713
3790
|
ceremony
|
|
@@ -3724,10 +3801,20 @@ class DynamicWalletClient {
|
|
|
3724
3801
|
});
|
|
3725
3802
|
return;
|
|
3726
3803
|
}
|
|
3804
|
+
if (shareSetType === 'delegated' && newThresholdSignatureScheme) {
|
|
3805
|
+
this.recordDelegatedShareSet({
|
|
3806
|
+
accountAddress,
|
|
3807
|
+
wallet,
|
|
3808
|
+
newShareSetId: shareSetId,
|
|
3809
|
+
newThresholdSignatureScheme
|
|
3810
|
+
});
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3727
3813
|
if (shareSetType !== 'rootUser') {
|
|
3728
|
-
// 'delegated'
|
|
3729
|
-
//
|
|
3730
|
-
//
|
|
3814
|
+
// 'delegated' (no newThresholdSignatureScheme, e.g. refresh) → SDK doesn't
|
|
3815
|
+
// own the otherShareSets[] write from refresh.
|
|
3816
|
+
// 'server' → internal server share set, not for SDK rotation.
|
|
3817
|
+
// undefined → server emitted partial payload (already warned upstream).
|
|
3731
3818
|
this.logger.info('[WaasShareSet] rotation skipped: non-rootUser share-set type', {
|
|
3732
3819
|
context: _extends({}, baseContext, {
|
|
3733
3820
|
shareSetType,
|
|
@@ -3754,6 +3841,38 @@ class DynamicWalletClient {
|
|
|
3754
3841
|
shareSetId
|
|
3755
3842
|
});
|
|
3756
3843
|
}
|
|
3844
|
+
// Records a new `delegated` share set under wallet.otherShareSets[],
|
|
3845
|
+
// replacing an existing delegated entry if one is already there (rather
|
|
3846
|
+
// than appending duplicates on retry). Only reachable from the reshare
|
|
3847
|
+
// delegation flow — refresh doesn't thread newThresholdSignatureScheme
|
|
3848
|
+
// so it falls through to the skip path before reaching here.
|
|
3849
|
+
recordDelegatedShareSet({ accountAddress, wallet, newShareSetId, newThresholdSignatureScheme }) {
|
|
3850
|
+
const walletProps = this.getWalletFromMap(accountAddress);
|
|
3851
|
+
var _walletProps_otherShareSets;
|
|
3852
|
+
const existing = (_walletProps_otherShareSets = walletProps == null ? void 0 : walletProps.otherShareSets) != null ? _walletProps_otherShareSets : [];
|
|
3853
|
+
const newEntry = {
|
|
3854
|
+
shareSetId: newShareSetId,
|
|
3855
|
+
shareSetType: 'delegated',
|
|
3856
|
+
thresholdSignatureScheme: newThresholdSignatureScheme,
|
|
3857
|
+
createdAt: new Date().toISOString()
|
|
3858
|
+
};
|
|
3859
|
+
const delegatedIdx = existing.findIndex((s)=>s.shareSetType === 'delegated');
|
|
3860
|
+
const updated = delegatedIdx >= 0 ? existing.map((s, i)=>i === delegatedIdx ? newEntry : s) : [
|
|
3861
|
+
...existing,
|
|
3862
|
+
newEntry
|
|
3863
|
+
];
|
|
3864
|
+
this.logger.info('[WaasShareSet] delegated shareSetId added by reshare ceremony', {
|
|
3865
|
+
context: {
|
|
3866
|
+
walletId: wallet.walletId,
|
|
3867
|
+
rootUserShareSetId: wallet.shareSetId,
|
|
3868
|
+
delegatedShareSetId: newShareSetId,
|
|
3869
|
+
replacedExistingDelegated: delegatedIdx >= 0
|
|
3870
|
+
}
|
|
3871
|
+
});
|
|
3872
|
+
this.updateWalletMap(accountAddress, {
|
|
3873
|
+
otherShareSets: updated
|
|
3874
|
+
});
|
|
3875
|
+
}
|
|
3757
3876
|
async internalReshare({ chainName, accountAddress, oldThresholdSignatureScheme, newThresholdSignatureScheme, password = undefined, signedSessionId, cloudProviders = [], delegateToProjectEnvironment = false, mfaToken, elevatedAccessToken, revokeDelegation = false, hasAttemptedKeyShareRecovery = false, googleDriveAccessToken }) {
|
|
3758
3877
|
const dynamicRequestId = uuid.v4();
|
|
3759
3878
|
// Password validation - wrapped in try-catch for consistent error handling
|
|
@@ -3796,7 +3915,8 @@ class DynamicWalletClient {
|
|
|
3796
3915
|
try {
|
|
3797
3916
|
const { existingClientShareCount } = core.getReshareConfig({
|
|
3798
3917
|
oldThresholdSignatureScheme,
|
|
3799
|
-
newThresholdSignatureScheme
|
|
3918
|
+
newThresholdSignatureScheme,
|
|
3919
|
+
delegateToProjectEnvironment: resolvedDelegation
|
|
3800
3920
|
});
|
|
3801
3921
|
const wallet = await this.getWallet({
|
|
3802
3922
|
accountAddress,
|
|
@@ -3810,11 +3930,14 @@ class DynamicWalletClient {
|
|
|
3810
3930
|
accountAddress,
|
|
3811
3931
|
wallet,
|
|
3812
3932
|
oldThresholdSignatureScheme,
|
|
3813
|
-
newThresholdSignatureScheme
|
|
3933
|
+
newThresholdSignatureScheme,
|
|
3934
|
+
delegateToProjectEnvironment: resolvedDelegation
|
|
3814
3935
|
});
|
|
3815
|
-
// Ensure existing client key shares
|
|
3816
|
-
|
|
3817
|
-
|
|
3936
|
+
// Ensure existing client key shares match what getReshareConfig expected.
|
|
3937
|
+
// For most flows this is >= 1; for 2/2 → 2/2 + delegation it's 0 (the
|
|
3938
|
+
// ceremony has no surviving client-side party from the old quorum).
|
|
3939
|
+
if (existingClientKeyShares.length < existingClientShareCount) {
|
|
3940
|
+
throw new Error(`Expected ${existingClientShareCount} existing client key share(s) for reshare operation but only found ${existingClientKeyShares.length} for account address: ${accountAddress}`);
|
|
3818
3941
|
}
|
|
3819
3942
|
const clientKeygenIds = [
|
|
3820
3943
|
...newClientKeygenIds,
|
|
@@ -3878,6 +4001,7 @@ class DynamicWalletClient {
|
|
|
3878
4001
|
ceremony: 'reshare',
|
|
3879
4002
|
shareSetId,
|
|
3880
4003
|
shareSetType,
|
|
4004
|
+
newThresholdSignatureScheme,
|
|
3881
4005
|
extraContext: {
|
|
3882
4006
|
oldThresholdSignatureScheme,
|
|
3883
4007
|
newThresholdSignatureScheme,
|
|
@@ -4001,41 +4125,12 @@ class DynamicWalletClient {
|
|
|
4001
4125
|
}
|
|
4002
4126
|
throw reshareError;
|
|
4003
4127
|
}
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
// Use effective* variables which may have been populated from existing wallet state
|
|
4011
|
-
if (resolvedDelegation && resolvedCloudProviders.length > 0) {
|
|
4012
|
-
// Delegation + Cloud Providers: Client's existing share backs up to both Dynamic and cloud providers.
|
|
4013
|
-
// The new share goes to the webhook for delegation.
|
|
4014
|
-
distribution = createDelegationWithCloudProviderDistribution({
|
|
4015
|
-
providers: resolvedCloudProviders,
|
|
4016
|
-
existingShares: existingReshareResults,
|
|
4017
|
-
delegatedShare: newReshareResults[0]
|
|
4018
|
-
});
|
|
4019
|
-
} else if (resolvedDelegation) {
|
|
4020
|
-
// Delegation only: Client's existing share backs up to Dynamic.
|
|
4021
|
-
// The new share goes to the webhook for delegation. No cloud provider backup.
|
|
4022
|
-
distribution = createDelegationOnlyDistribution({
|
|
4023
|
-
existingShares: existingReshareResults,
|
|
4024
|
-
delegatedShare: newReshareResults[0]
|
|
4025
|
-
});
|
|
4026
|
-
} else if (resolvedCloudProviders.length > 0) {
|
|
4027
|
-
// Cloud Providers only: Split shares between Dynamic (N-1) and cloud providers (1).
|
|
4028
|
-
// The last share (new share) goes to cloud providers.
|
|
4029
|
-
distribution = createCloudProviderDistribution({
|
|
4030
|
-
providers: resolvedCloudProviders,
|
|
4031
|
-
allShares: allClientShares
|
|
4032
|
-
});
|
|
4033
|
-
} else {
|
|
4034
|
-
// No delegation, no cloud providers: All shares go to Dynamic backend only.
|
|
4035
|
-
distribution = createDynamicOnlyDistribution({
|
|
4036
|
-
allShares: allClientShares
|
|
4037
|
-
});
|
|
4038
|
-
}
|
|
4128
|
+
const distribution = selectReshareDistribution({
|
|
4129
|
+
resolvedDelegation,
|
|
4130
|
+
resolvedCloudProviders,
|
|
4131
|
+
existingReshareResults,
|
|
4132
|
+
newReshareResults
|
|
4133
|
+
});
|
|
4039
4134
|
this.updateWalletMap(accountAddress, {
|
|
4040
4135
|
thresholdSignatureScheme: newThresholdSignatureScheme
|
|
4041
4136
|
});
|
|
@@ -4094,10 +4189,22 @@ class DynamicWalletClient {
|
|
|
4094
4189
|
});
|
|
4095
4190
|
// Store client key shares to storage (localStorage or secureStorage)
|
|
4096
4191
|
// only after backup succeeds.
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4192
|
+
//
|
|
4193
|
+
// Share-set delegation (FF=on) skips this entirely — the ceremony
|
|
4194
|
+
// output's refreshed rootUser-client share went to the webhook
|
|
4195
|
+
// (delegatedShare), and the original gen1 rootUser-client share must
|
|
4196
|
+
// stay in local cache so the user can still sign rootUser. See the
|
|
4197
|
+
// mixed-share-sign isolation property tested in
|
|
4198
|
+
// `wallet-service/scripts/twoOfTwoReshareExperiment.ts`. Calling this
|
|
4199
|
+
// with `clientShares: []` would clear the local cache (per the
|
|
4200
|
+
// method's "full replacement" semantics), breaking signing until
|
|
4201
|
+
// recovery refetches from Dynamic.
|
|
4202
|
+
if (distribution.clientShares.length > 0) {
|
|
4203
|
+
await this.setClientKeySharesToStorage({
|
|
4204
|
+
accountAddress,
|
|
4205
|
+
clientKeyShares: distribution.clientShares
|
|
4206
|
+
});
|
|
4207
|
+
}
|
|
4101
4208
|
// Operation summary — correlates against downstream sign failures /
|
|
4102
4209
|
// pending-rotation reports. `ceremonyCallbackFired: false` here means
|
|
4103
4210
|
// either the server is legacy (didn't emit ceremony_complete) or the
|
|
@@ -4139,7 +4246,7 @@ class DynamicWalletClient {
|
|
|
4139
4246
|
throw error;
|
|
4140
4247
|
}
|
|
4141
4248
|
}
|
|
4142
|
-
async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, operationName }) {
|
|
4249
|
+
async performDelegationOperation({ accountAddress, password, signedSessionId, mfaToken, newThresholdSignatureScheme, revokeDelegation = false, useShareSetReshare = false, operationName }) {
|
|
4143
4250
|
try {
|
|
4144
4251
|
const delegateToProjectEnvironment = this.featureFlags && this.featureFlags[core.FEATURE_FLAGS.ENABLE_DELEGATED_KEY_SHARES_FLAG] === true;
|
|
4145
4252
|
if (!delegateToProjectEnvironment) {
|
|
@@ -4156,8 +4263,11 @@ class DynamicWalletClient {
|
|
|
4156
4263
|
}
|
|
4157
4264
|
const walletData = this.getWalletFromMap(accountAddress);
|
|
4158
4265
|
const currentThresholdSignatureScheme = walletData.thresholdSignatureScheme;
|
|
4159
|
-
//
|
|
4160
|
-
|
|
4266
|
+
// Under the share-set reshare flow, rootUser is not mutated — the
|
|
4267
|
+
// delegation creates a separate `delegated` share set. So we don't
|
|
4268
|
+
// re-pin existing cloud providers; preservation is irrelevant when the
|
|
4269
|
+
// primary share set is untouched.
|
|
4270
|
+
const activeProviders = useShareSetReshare ? [] : getActiveCloudProviders(walletData == null ? void 0 : walletData.clientKeySharesBackupInfo);
|
|
4161
4271
|
await this.reshare({
|
|
4162
4272
|
chainName: walletData.chainName,
|
|
4163
4273
|
accountAddress,
|
|
@@ -4182,19 +4292,82 @@ class DynamicWalletClient {
|
|
|
4182
4292
|
}
|
|
4183
4293
|
}
|
|
4184
4294
|
async delegateKeyShares({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
4295
|
+
var _this_featureFlags;
|
|
4296
|
+
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[core.FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4297
|
+
if (useShareSetReshare) {
|
|
4298
|
+
var _this_getWalletFromMap;
|
|
4299
|
+
// FF on: delegation creates a separate `delegated` share set without
|
|
4300
|
+
// touching rootUser. If the wallet already has one, this is a no-op.
|
|
4301
|
+
const existingDelegated = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
|
|
4302
|
+
if (existingDelegated) {
|
|
4303
|
+
var _this_getWalletFromMap1;
|
|
4304
|
+
this.logger.info('[WaasShareSet] delegateKeyShares no-op — wallet already has a delegated share set', {
|
|
4305
|
+
accountAddress,
|
|
4306
|
+
delegatedShareSetId: existingDelegated.shareSetId
|
|
4307
|
+
});
|
|
4308
|
+
const backupInfo = (_this_getWalletFromMap1 = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap1.clientKeySharesBackupInfo;
|
|
4309
|
+
return (backupInfo == null ? void 0 : backupInfo.backups[core.BackupLocation.DELEGATED]) || [];
|
|
4310
|
+
}
|
|
4311
|
+
this.logger.info('[WaasShareSet] delegateKeyShares using share-set reshare path', {
|
|
4312
|
+
accountAddress
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4185
4315
|
await this.performDelegationOperation({
|
|
4186
4316
|
accountAddress,
|
|
4187
4317
|
password,
|
|
4188
4318
|
signedSessionId,
|
|
4189
4319
|
mfaToken,
|
|
4190
|
-
|
|
4191
|
-
|
|
4320
|
+
// FF on: reshare to 2/2 — the new delegated share set is always 2/2
|
|
4321
|
+
// (server + delegated webhook). getReshareConfig now has a 2/2 → 2/2
|
|
4322
|
+
// + delegation case that returns newClientShareCount=1 (the share
|
|
4323
|
+
// routed to the webhook) so the ceremony generates it correctly.
|
|
4324
|
+
// FF off: legacy 2/2 → 2/3 reshare that mutates rootUser.
|
|
4325
|
+
newThresholdSignatureScheme: useShareSetReshare ? core.ThresholdSignatureScheme.TWO_OF_TWO : core.ThresholdSignatureScheme.TWO_OF_THREE,
|
|
4326
|
+
operationName: 'delegateKeyShares',
|
|
4327
|
+
useShareSetReshare
|
|
4192
4328
|
});
|
|
4193
4329
|
const backupInfo = this.getWalletFromMap(accountAddress).clientKeySharesBackupInfo;
|
|
4194
4330
|
const delegatedKeyShares = backupInfo.backups[core.BackupLocation.DELEGATED] || [];
|
|
4195
4331
|
return delegatedKeyShares;
|
|
4196
4332
|
}
|
|
4197
4333
|
async revokeDelegation({ accountAddress, password = undefined, signedSessionId, mfaToken }) {
|
|
4334
|
+
var _this_featureFlags;
|
|
4335
|
+
const useShareSetReshare = ((_this_featureFlags = this.featureFlags) == null ? void 0 : _this_featureFlags[core.FEATURE_FLAGS.ENABLE_SHARE_SET_RESHARE]) === true;
|
|
4336
|
+
if (useShareSetReshare) {
|
|
4337
|
+
var _this_getWalletFromMap;
|
|
4338
|
+
const wallet = await this.getWallet({
|
|
4339
|
+
accountAddress,
|
|
4340
|
+
walletOperation: core.WalletOperation.NO_OPERATION,
|
|
4341
|
+
password,
|
|
4342
|
+
signedSessionId
|
|
4343
|
+
});
|
|
4344
|
+
const delegatedShareSet = getDelegatedShareSet((_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets);
|
|
4345
|
+
if (!delegatedShareSet) {
|
|
4346
|
+
this.logger.info('[WaasShareSet] revokeDelegation called but no delegated share set found', {
|
|
4347
|
+
accountAddress
|
|
4348
|
+
});
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
this.logger.info('[WaasShareSet] revokeDelegation via share-set REST DELETE (no MPC ceremony)', {
|
|
4352
|
+
accountAddress,
|
|
4353
|
+
delegatedShareSetId: delegatedShareSet.shareSetId,
|
|
4354
|
+
walletId: wallet.walletId
|
|
4355
|
+
});
|
|
4356
|
+
await this.apiClient.revokeDelegation({
|
|
4357
|
+
walletId: wallet.walletId,
|
|
4358
|
+
shareSetId: delegatedShareSet.shareSetId,
|
|
4359
|
+
signedSessionId,
|
|
4360
|
+
dynamicRequestId: uuid.v4().replaceAll('-', '')
|
|
4361
|
+
});
|
|
4362
|
+
// Refresh wallet state so otherShareSets / backups reflect the cleared delegation
|
|
4363
|
+
await this.getWallet({
|
|
4364
|
+
accountAddress,
|
|
4365
|
+
walletOperation: core.WalletOperation.NO_OPERATION,
|
|
4366
|
+
password,
|
|
4367
|
+
signedSessionId
|
|
4368
|
+
});
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4198
4371
|
await this.performDelegationOperation({
|
|
4199
4372
|
accountAddress,
|
|
4200
4373
|
password,
|
|
@@ -4741,7 +4914,7 @@ class DynamicWalletClient {
|
|
|
4741
4914
|
hasDelegatedShare: !!distribution.delegatedShare
|
|
4742
4915
|
}));
|
|
4743
4916
|
try {
|
|
4744
|
-
var _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
4917
|
+
var _this_getWalletFromMap, _backupData_locationsWithKeyShares, _backupData_locationsWithKeyShares1;
|
|
4745
4918
|
// `let` so the retry path can swap in a freshly-signed session via the reverse channel on 400.
|
|
4746
4919
|
let resolvedSignedSessionId = await this.resolveSignedSessionId(signedSessionId);
|
|
4747
4920
|
const canRefreshSignedSessionId = this.getSignedSessionIdCallback !== undefined;
|
|
@@ -4836,10 +5009,24 @@ class DynamicWalletClient {
|
|
|
4836
5009
|
googleDriveAccessToken
|
|
4837
5010
|
}));
|
|
4838
5011
|
}
|
|
5012
|
+
// When this ceremony minted a separate `delegated` share set (FF=on
|
|
5013
|
+
// path), backup activation must target that share set — not rootUser —
|
|
5014
|
+
// so `/delegatedAccess/delivery` and `/backup/locations` resolve to the
|
|
5015
|
+
// pending delegated row instead of mutating rootUser. The FF=off path
|
|
5016
|
+
// leaves `otherShareSets` empty and falls back to rootUser, matching
|
|
5017
|
+
// legacy in-place delegation behavior.
|
|
5018
|
+
//
|
|
5019
|
+
// Read otherShareSets fresh from the wallet map — `recordDelegatedShareSet`
|
|
5020
|
+
// wrote the new entry during `ceremony_complete`, which fired between
|
|
5021
|
+
// when `walletData` was captured at the top of this method and now.
|
|
5022
|
+
const freshOtherShareSets = (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.otherShareSets;
|
|
5023
|
+
const delegatedShareSet = distribution.delegatedShare ? getDelegatedShareSet(freshOtherShareSets) : undefined;
|
|
5024
|
+
var _delegatedShareSet_shareSetId;
|
|
5025
|
+
const targetShareSetId = (_delegatedShareSet_shareSetId = delegatedShareSet == null ? void 0 : delegatedShareSet.shareSetId) != null ? _delegatedShareSet_shareSetId : walletData.shareSetId;
|
|
4839
5026
|
if (distribution.delegatedShare) {
|
|
4840
5027
|
uploadPromises.push(retryPromise(()=>this.publishDelegatedShare({
|
|
4841
5028
|
walletId: walletData.walletId,
|
|
4842
|
-
shareSetId:
|
|
5029
|
+
shareSetId: targetShareSetId,
|
|
4843
5030
|
delegatedShare: distribution.delegatedShare,
|
|
4844
5031
|
signedSessionId: resolvedSignedSessionId,
|
|
4845
5032
|
dynamicRequestId,
|
|
@@ -4869,7 +5056,10 @@ class DynamicWalletClient {
|
|
|
4869
5056
|
// won't fix.
|
|
4870
5057
|
const backupData = await retryPromise(()=>this.apiClient.markKeySharesAsBackedUp({
|
|
4871
5058
|
walletId: walletData.walletId,
|
|
4872
|
-
|
|
5059
|
+
// Same routing rule as publishDelegatedShare above: when this
|
|
5060
|
+
// ceremony minted a separate `delegated` share set, the atomic
|
|
5061
|
+
// swap in /backup/locations must activate THAT row, not rootUser.
|
|
5062
|
+
shareSetId: targetShareSetId,
|
|
4873
5063
|
locations,
|
|
4874
5064
|
dynamicRequestId,
|
|
4875
5065
|
passwordUpdateBatchId
|
|
@@ -5267,13 +5457,35 @@ class DynamicWalletClient {
|
|
|
5267
5457
|
}
|
|
5268
5458
|
async decryptKeyShare({ keyShare, password }) {
|
|
5269
5459
|
const decodedKeyShare = JSON.parse(Buffer.from(keyShare, 'base64').toString());
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
const
|
|
5276
|
-
|
|
5460
|
+
// Track whether a user-supplied password was provided so we can emit a
|
|
5461
|
+
// distinct error class on failure. The default `environmentId` fallback is
|
|
5462
|
+
// applied here (and only here) so the original `password` argument retains
|
|
5463
|
+
// the caller's intent — see `recoverEncryptedBackupByWallet` / `getWallet`,
|
|
5464
|
+
// which no longer apply their own fallback.
|
|
5465
|
+
const usedDefaultPassword = !password;
|
|
5466
|
+
const effectivePassword = password != null ? password : this.environmentId;
|
|
5467
|
+
try {
|
|
5468
|
+
const decryptedKeyShare = await decryptData({
|
|
5469
|
+
data: decodedKeyShare,
|
|
5470
|
+
password: effectivePassword
|
|
5471
|
+
});
|
|
5472
|
+
this.logPasswordSharePresence(password, 'decrypt');
|
|
5473
|
+
const deserializedKeyShare = JSON.parse(decryptedKeyShare);
|
|
5474
|
+
return deserializedKeyShare;
|
|
5475
|
+
} catch (error) {
|
|
5476
|
+
// When no user password was supplied, an InvalidPasswordError is
|
|
5477
|
+
// misleading — the user never entered a password, so the "wrong password"
|
|
5478
|
+
// framing doesn't apply. Re-throw as a distinct system-side error so
|
|
5479
|
+
// dashboards/logs classify it as `decryption_failure` instead of
|
|
5480
|
+
// `wrong_password`, and surface that the cause is most likely an
|
|
5481
|
+
// environment-ID mismatch (e.g. wallet encrypted in another environment).
|
|
5482
|
+
if (usedDefaultPassword && error instanceof InvalidPasswordError) {
|
|
5483
|
+
throw new KeyShareDecryptionError({
|
|
5484
|
+
cause: error
|
|
5485
|
+
});
|
|
5486
|
+
}
|
|
5487
|
+
throw error;
|
|
5488
|
+
}
|
|
5277
5489
|
}
|
|
5278
5490
|
/**
|
|
5279
5491
|
* Validates that the provided password is consistent with existing encrypted wallets.
|
|
@@ -5529,9 +5741,12 @@ class DynamicWalletClient {
|
|
|
5529
5741
|
userId: this.userId
|
|
5530
5742
|
});
|
|
5531
5743
|
const dynamicKeyShares = data.keyShares.filter((keyShare)=>keyShare.encryptedAccountCredential !== null && keyShare.backupLocation === core.BackupLocation.DYNAMIC);
|
|
5532
|
-
const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)
|
|
5744
|
+
const decryptedKeyShares = await Promise.all(dynamicKeyShares.map((keyShare)=>// `decryptKeyShare` owns the `environmentId` fallback; passing
|
|
5745
|
+
// `password` (possibly undefined) lets it distinguish a user-supplied
|
|
5746
|
+
// password failure from an envId-fallback failure.
|
|
5747
|
+
this.decryptKeyShare({
|
|
5533
5748
|
keyShare: keyShare.encryptedAccountCredential,
|
|
5534
|
-
password
|
|
5749
|
+
password
|
|
5535
5750
|
})));
|
|
5536
5751
|
if (storeRecoveredShares) {
|
|
5537
5752
|
await this.setClientKeySharesToStorage({
|
|
@@ -6232,9 +6447,11 @@ class DynamicWalletClient {
|
|
|
6232
6447
|
return wallet;
|
|
6233
6448
|
}
|
|
6234
6449
|
// TODO(zfaizal2): throw error if signedSessionId is not provided after service deploy
|
|
6450
|
+
// `decryptKeyShare` owns the environmentId fallback so the original
|
|
6451
|
+
// `password` (possibly undefined) propagates here.
|
|
6235
6452
|
const decryptedKeyShares = await this.recoverEncryptedBackupByWallet({
|
|
6236
6453
|
accountAddress,
|
|
6237
|
-
password
|
|
6454
|
+
password,
|
|
6238
6455
|
walletOperation: walletOperation,
|
|
6239
6456
|
signedSessionId,
|
|
6240
6457
|
shareCount
|