Skip to content

Commit ef69e42

Browse files
feat(express): migrate update keychain passphrase to typed routes
2 parents 03cc0c2 + 7f3cb74 commit ef69e42

File tree

5 files changed

+267
-46
lines changed

5 files changed

+267
-46
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,24 +1024,24 @@ export async function handleWalletUpdate(
10241024
* Changes a keychain's passphrase, re-encrypting the key to a new password
10251025
* @param req
10261026
*/
1027-
export async function handleKeychainChangePassword(req: express.Request): Promise<unknown> {
1028-
const { oldPassword, newPassword, otp } = req.body;
1029-
if (!oldPassword || !newPassword) {
1030-
throw new ApiResponseError('Missing 1 or more required fields: [oldPassword, newPassword]', 400);
1031-
}
1027+
export async function handleKeychainChangePassword(
1028+
req: ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>
1029+
): Promise<unknown> {
1030+
const { oldPassword, newPassword, otp, coin: coinName, id } = req.decoded;
10321031
const reqId = new RequestTracer();
10331032

10341033
const bitgo = req.bitgo;
1035-
const coin = bitgo.coin(req.params.coin);
1034+
const coin = bitgo.coin(coinName);
10361035

10371036
if (otp) {
10381037
await bitgo.unlock({ otp });
10391038
}
10401039

10411040
const keychain = await coin.keychains().get({
1042-
id: req.params.id,
1041+
id: id,
10431042
reqId,
10441043
});
1044+
10451045
if (!keychain) {
10461046
throw new ApiResponseError(`Keychain ${req.params.id} not found`, 404);
10471047
}
@@ -1612,12 +1612,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16121612
router.put('express.wallet.update', [prepareBitGo(config), typedPromiseWrapper(handleWalletUpdate)]);
16131613

16141614
// change wallet passphrase
1615-
app.post(
1616-
'/api/v2/:coin/keychain/:id/changepassword',
1617-
parseBody,
1615+
router.post('express.keychain.changePassword', [
16181616
prepareBitGo(config),
1619-
promiseWrapper(handleKeychainChangePassword)
1620-
);
1617+
typedPromiseWrapper(handleKeychainChangePassword),
1618+
]);
16211619

16221620
router.post('express.v2.wallet.createAddress', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateAddress)]);
16231621

modules/express/src/typedRoutes/api/index.ts

Lines changed: 152 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { PutPendingApproval } from './v1/pendingApproval';
1515
import { PostSignTransaction } from './v1/signTransaction';
1616
import { PostKeychainLocal } from './v2/keychainLocal';
1717
import { GetLightningState } from './v2/lightningState';
18+
import { PostKeychainChangePassword } from './v2/keychainChangePassword';
1819
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1920
import { PostUnlockLightningWallet } from './v2/unlockWallet';
2021
import { PostVerifyCoinAddress } from './v2/verifyAddress';
@@ -32,73 +33,151 @@ import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
3233
import { PostShareWallet } from './v2/shareWallet';
3334
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';
3435

35-
export const ExpressApi = apiSpec({
36+
// Too large types can cause the following error
37+
//
38+
// > error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
39+
//
40+
// As a workaround, only construct expressApi with a single key and add it to the type union at the end
41+
42+
export const ExpressPingApiSpec = apiSpec({
3643
'express.ping': {
3744
get: GetPing,
3845
},
46+
});
47+
48+
export const ExpressPingExpressApiSpec = apiSpec({
3949
'express.pingExpress': {
4050
get: GetPingExpress,
4151
},
52+
});
53+
54+
export const ExpressLoginApiSpec = apiSpec({
4255
'express.login': {
4356
post: PostLogin,
4457
},
58+
});
59+
60+
export const ExpressDecryptApiSpec = apiSpec({
4561
'express.decrypt': {
4662
post: PostDecrypt,
4763
},
64+
});
65+
66+
export const ExpressEncryptApiSpec = apiSpec({
4867
'express.encrypt': {
4968
post: PostEncrypt,
5069
},
70+
});
71+
72+
export const ExpressVerifyAddressApiSpec = apiSpec({
5173
'express.verifyaddress': {
5274
post: PostVerifyAddress,
5375
},
76+
});
77+
78+
export const ExpressVerifyCoinAddressApiSpec = apiSpec({
79+
'express.verifycoinaddress': {
80+
post: PostVerifyCoinAddress,
81+
},
82+
});
83+
84+
export const ExpressCalculateMinerFeeInfoApiSpec = apiSpec({
85+
'express.calculateminerfeeinfo': {
86+
post: PostCalculateMinerFeeInfo,
87+
},
88+
});
89+
90+
export const ExpressV1WalletAcceptShareApiSpec = apiSpec({
5491
'express.v1.wallet.acceptShare': {
5592
post: PostAcceptShare,
5693
},
94+
});
95+
96+
export const ExpressV1WalletSimpleCreateApiSpec = apiSpec({
5797
'express.v1.wallet.simplecreate': {
5898
post: PostSimpleCreate,
5999
},
100+
});
101+
102+
export const ExpressV1PendingApprovalsApiSpec = apiSpec({
60103
'express.v1.pendingapprovals': {
61104
put: PutPendingApproval,
62105
},
106+
});
107+
108+
export const ExpressV1WalletSignTransactionApiSpec = apiSpec({
63109
'express.v1.wallet.signTransaction': {
64110
post: PostSignTransaction,
65111
},
66-
'express.keychain.local': {
67-
post: PostKeychainLocal,
68-
},
69-
'express.lightning.getState': {
70-
get: GetLightningState,
71-
},
72-
'express.lightning.initWallet': {
73-
post: PostLightningInitWallet,
74-
},
75-
'express.lightning.unlockWallet': {
76-
post: PostUnlockLightningWallet,
77-
},
78-
'express.verifycoinaddress': {
79-
post: PostVerifyCoinAddress,
80-
},
81-
'express.v2.wallet.createAddress': {
82-
post: PostCreateAddress,
83-
},
84-
'express.calculateminerfeeinfo': {
85-
post: PostCalculateMinerFeeInfo,
86-
},
112+
});
113+
114+
export const ExpressV1KeychainDeriveApiSpec = apiSpec({
87115
'express.v1.keychain.derive': {
88116
post: PostDeriveLocalKeyChain,
89117
},
118+
});
119+
120+
export const ExpressV1KeychainLocalApiSpec = apiSpec({
90121
'express.v1.keychain.local': {
91122
post: PostCreateLocalKeyChain,
92123
},
124+
});
125+
126+
export const ExpressV1PendingApprovalConstructTxApiSpec = apiSpec({
93127
'express.v1.pendingapproval.constructTx': {
94128
put: PutConstructPendingApprovalTx,
95129
},
130+
});
131+
132+
export const ExpressV1WalletConsolidateUnspentsApiSpec = apiSpec({
96133
'express.v1.wallet.consolidateunspents': {
97134
put: PutConsolidateUnspents,
98135
},
136+
});
137+
138+
export const ExpressV1WalletFanoutUnspentsApiSpec = apiSpec({
99139
'express.v1.wallet.fanoutunspents': {
100140
put: PutFanoutUnspents,
101141
},
142+
});
143+
144+
export const ExpressV2WalletCreateAddressApiSpec = apiSpec({
145+
'express.v2.wallet.createAddress': {
146+
post: PostCreateAddress,
147+
},
148+
});
149+
150+
export const ExpressKeychainLocalApiSpec = apiSpec({
151+
'express.keychain.local': {
152+
post: PostKeychainLocal,
153+
},
154+
});
155+
156+
export const ExpressKeychainChangePasswordApiSpec = apiSpec({
157+
'express.keychain.changePassword': {
158+
post: PostKeychainChangePassword,
159+
},
160+
});
161+
162+
export const ExpressLightningGetStateApiSpec = apiSpec({
163+
'express.lightning.getState': {
164+
get: GetLightningState,
165+
},
166+
});
167+
168+
export const ExpressLightningInitWalletApiSpec = apiSpec({
169+
'express.lightning.initWallet': {
170+
post: PostLightningInitWallet,
171+
},
172+
});
173+
174+
export const ExpressLightningUnlockWalletApiSpec = apiSpec({
175+
'express.lightning.unlockWallet': {
176+
post: PostUnlockLightningWallet,
177+
},
178+
});
179+
180+
export const ExpressOfcSignPayloadApiSpec = apiSpec({
102181
'express.ofc.signPayload': {
103182
post: PostOfcSignPayload,
104183
},
@@ -122,7 +201,57 @@ export const ExpressApi = apiSpec({
122201
},
123202
});
124203

125-
export type ExpressApi = typeof ExpressApi;
204+
export type ExpressApi = typeof ExpressPingApiSpec &
205+
typeof ExpressPingExpressApiSpec &
206+
typeof ExpressLoginApiSpec &
207+
typeof ExpressDecryptApiSpec &
208+
typeof ExpressEncryptApiSpec &
209+
typeof ExpressVerifyAddressApiSpec &
210+
typeof ExpressVerifyCoinAddressApiSpec &
211+
typeof ExpressCalculateMinerFeeInfoApiSpec &
212+
typeof ExpressV1WalletAcceptShareApiSpec &
213+
typeof ExpressV1WalletSimpleCreateApiSpec &
214+
typeof ExpressV1PendingApprovalsApiSpec &
215+
typeof ExpressV1WalletSignTransactionApiSpec &
216+
typeof ExpressV1KeychainDeriveApiSpec &
217+
typeof ExpressV1KeychainLocalApiSpec &
218+
typeof ExpressV1PendingApprovalConstructTxApiSpec &
219+
typeof ExpressV1WalletConsolidateUnspentsApiSpec &
220+
typeof ExpressV1WalletFanoutUnspentsApiSpec &
221+
typeof ExpressV2WalletCreateAddressApiSpec &
222+
typeof ExpressKeychainLocalApiSpec &
223+
typeof ExpressKeychainChangePasswordApiSpec &
224+
typeof ExpressLightningGetStateApiSpec &
225+
typeof ExpressLightningInitWalletApiSpec &
226+
typeof ExpressLightningUnlockWalletApiSpec &
227+
typeof ExpressOfcSignPayloadApiSpec;
228+
229+
export const ExpressApi: ExpressApi = {
230+
...ExpressPingApiSpec,
231+
...ExpressPingExpressApiSpec,
232+
...ExpressLoginApiSpec,
233+
...ExpressDecryptApiSpec,
234+
...ExpressEncryptApiSpec,
235+
...ExpressVerifyAddressApiSpec,
236+
...ExpressVerifyCoinAddressApiSpec,
237+
...ExpressCalculateMinerFeeInfoApiSpec,
238+
...ExpressV1WalletAcceptShareApiSpec,
239+
...ExpressV1WalletSimpleCreateApiSpec,
240+
...ExpressV1PendingApprovalsApiSpec,
241+
...ExpressV1WalletSignTransactionApiSpec,
242+
...ExpressV1KeychainDeriveApiSpec,
243+
...ExpressV1KeychainLocalApiSpec,
244+
...ExpressV1PendingApprovalConstructTxApiSpec,
245+
...ExpressV1WalletConsolidateUnspentsApiSpec,
246+
...ExpressV1WalletFanoutUnspentsApiSpec,
247+
...ExpressV2WalletCreateAddressApiSpec,
248+
...ExpressKeychainLocalApiSpec,
249+
...ExpressKeychainChangePasswordApiSpec,
250+
...ExpressLightningGetStateApiSpec,
251+
...ExpressLightningInitWalletApiSpec,
252+
...ExpressLightningUnlockWalletApiSpec,
253+
...ExpressOfcSignPayloadApiSpec,
254+
};
126255

127256
type ExtractDecoded<T> = T extends t.Type<any, infer O, any> ? O : never;
128257
type FlattenDecoded<T> = T extends Record<string, unknown>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for changing a keychain's password
7+
*/
8+
export const KeychainChangePasswordParams = {
9+
/** Coin identifier (e.g. btc, tbtc, eth) */
10+
coin: t.string,
11+
/** The keychain id */
12+
id: t.string,
13+
} as const;
14+
15+
/**
16+
* Request body for changing a keychain's password
17+
*/
18+
export const KeychainChangePasswordBody = {
19+
/** The old password used to encrypt the keychain */
20+
oldPassword: t.string,
21+
/** The new password to re-encrypt the keychain */
22+
newPassword: t.string,
23+
/** Optional OTP to unlock the session if required */
24+
otp: optional(t.string),
25+
} as const;
26+
27+
/**
28+
* Response for changing a keychain's password
29+
*/
30+
export const KeychainChangePasswordResponse = {
31+
/** Successful update */
32+
200: t.unknown,
33+
/** Invalid request or not found */
34+
400: BitgoExpressError,
35+
404: BitgoExpressError,
36+
} as const;
37+
38+
/**
39+
* Change a keychain's passphrase, re-encrypting the key to a new password.
40+
*
41+
* @operationId express.v2.keychain.changePassword
42+
*/
43+
export const PostKeychainChangePassword = httpRoute({
44+
path: '/api/v2/{coin}/keychain/{id}/changepassword',
45+
method: 'POST',
46+
request: httpRequest({
47+
params: KeychainChangePasswordParams,
48+
body: KeychainChangePasswordBody,
49+
}),
50+
response: KeychainChangePasswordResponse,
51+
});

modules/express/test/unit/clientRoutes/changeKeychainPassword.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import * as sinon from 'sinon';
2-
32
import 'should-http';
43
import 'should-sinon';
54
import '../../lib/asserts';
6-
7-
import * as express from 'express';
8-
95
import { handleKeychainChangePassword } from '../../../src/clientRoutes';
10-
116
import { BitGo } from 'bitgo';
7+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
8+
import { decodeOrElse } from '@bitgo/sdk-core';
9+
import { KeychainChangePasswordResponse } from '../../../src/typedRoutes/api/v2/keychainChangePassword';
1210

1311
describe('Change Wallet Password', function () {
1412
it('should change wallet password', async function () {
@@ -36,19 +34,32 @@ describe('Change Wallet Password', function () {
3634
}),
3735
});
3836

37+
const coin = 'talgo';
38+
const id = '23423423423423';
39+
const oldPassword = 'oldPasswordString';
40+
const newPassword = 'newPasswordString';
3941
const mockRequest = {
4042
bitgo: stubBitgo,
4143
params: {
42-
coin: 'talgo',
43-
id: '23423423423423',
44+
coin,
45+
id,
4446
},
4547
body: {
46-
oldPassword: 'oldPasswordString',
47-
newPassword: 'newPasswordString',
48+
oldPassword,
49+
newPassword,
4850
},
49-
};
51+
decoded: {
52+
oldPassword,
53+
newPassword,
54+
coin,
55+
id,
56+
},
57+
} as unknown as ExpressApiRouteRequest<'express.keychain.changePassword', 'post'>;
5058

51-
const result = await handleKeychainChangePassword(mockRequest as express.Request & typeof mockRequest);
59+
const result = await handleKeychainChangePassword(mockRequest);
60+
decodeOrElse('KeychainChangePasswordResponse200', KeychainChangePasswordResponse[200], result, (errors) => {
61+
throw new Error(`Response did not match expected codec: ${errors}`);
62+
});
5263
({ result: '200 OK' }).should.be.eql(result);
5364
});
5465
});

0 commit comments

Comments
 (0)