Skip to content

Commit dfd264d

Browse files
committed
feat: add bitgo api intergration to MBE
Ticket: WP-4593
1 parent a3b3a73 commit dfd264d

File tree

6 files changed

+9699
-158
lines changed

6 files changed

+9699
-158
lines changed

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
"generate-test-ssl": "openssl req -x509 -newkey rsa:2048 -keyout test-ssl-key.pem -out test-ssl-cert.pem -days 365 -nodes -subj '/CN=localhost'"
1818
},
1919
"dependencies": {
20+
"bitgo": "^44.2.0",
21+
"@bitgo/sdk-core": "^33.2.0",
2022
"body-parser": "^1.20.3",
2123
"connect-timeout": "^1.9.0",
2224
"debug": "^3.1.0",
2325
"express": "4.17.3",
2426
"lodash": "^4.17.20",
2527
"morgan": "^1.9.1",
26-
"superagent": "^8.0.9"
28+
"superagent": "^8.0.9",
29+
"proxy-agent": "6.4.0",
30+
"proxyquire": "^2.1.3"
2731
},
2832
"devDependencies": {
2933
"@types/body-parser": "^1.17.0",
@@ -54,6 +58,6 @@
5458
"typescript": "^4.2.4"
5559
},
5660
"engines": {
57-
"node": ">=14"
61+
"node": ">=20.18.0"
5862
}
5963
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as superagent from 'superagent';
2+
import debug from 'debug';
3+
import { config } from '../config';
4+
import { isMasterExpressConfig } from '../types';
5+
6+
const debugLogger = debug('bitgo:express:enclavedExpressClient');
7+
8+
interface CreateIndependentKeychainParams {
9+
source: 'user' | 'backup';
10+
coin?: string;
11+
type: 'independent';
12+
seed?: string;
13+
}
14+
15+
export interface IndependentKeychainResponse {
16+
id: string;
17+
pub: string;
18+
encryptedPrv?: string;
19+
type: 'independent';
20+
source: 'user' | 'backup' | 'bitgo';
21+
coin: string;
22+
}
23+
24+
export class EnclavedExpressClient {
25+
private readonly url: string;
26+
private readonly sslCert: string;
27+
private readonly coin?: string;
28+
29+
constructor(coin?: string) {
30+
const cfg = config();
31+
if (!isMasterExpressConfig(cfg)) {
32+
throw new Error('Configuration is not in master express mode');
33+
}
34+
35+
if (!cfg.enclavedExpressUrl || !cfg.enclavedExpressSSLCert) {
36+
throw new Error(
37+
'Enclaved Express URL not configured. Please set BITGO_ENCLAVED_EXPRESS_URL and BITGO_ENCLAVED_EXPRESS_SSL_CERT in your environment.',
38+
);
39+
}
40+
41+
this.url = cfg.enclavedExpressUrl;
42+
this.sslCert = cfg.enclavedExpressSSLCert;
43+
this.coin = coin;
44+
debugLogger('EnclavedExpressClient initialized with URL: %s', this.url);
45+
}
46+
47+
async ping(): Promise<void> {
48+
try {
49+
debugLogger('Pinging enclaved express at %s', this.url);
50+
await superagent.get(`${this.url}/ping`).ca(this.sslCert).send();
51+
} catch (error) {
52+
const err = error as Error;
53+
debugLogger('Failed to ping enclaved express: %s', err.message);
54+
throw new Error(`Failed to ping enclaved express: ${err.message}`);
55+
}
56+
}
57+
58+
/**
59+
* Create an independent multisig key for a given source and coin
60+
*/
61+
async createIndependentKeychain(
62+
params: CreateIndependentKeychainParams,
63+
): Promise<IndependentKeychainResponse> {
64+
if (!this.coin) {
65+
throw new Error('Coin not configured');
66+
}
67+
try {
68+
debugLogger('Creating independent keychain for coin: %s', this.coin);
69+
const { body: keychain } = await superagent
70+
.post(`${this.url}/api/${this.coin}/key/independent`)
71+
.ca(this.sslCert)
72+
.type('json')
73+
.send(params);
74+
return keychain;
75+
} catch (error) {
76+
const err = error as Error;
77+
debugLogger('Failed to create independent keychain: %s', err.message);
78+
throw new Error(`Failed to create independent keychain: ${err.message}`);
79+
}
80+
}
81+
}
82+
83+
/**
84+
* Create an enclaved express client if the configuration is present
85+
*/
86+
export function createEnclavedExpressClient(coin?: string): EnclavedExpressClient | undefined {
87+
try {
88+
return new EnclavedExpressClient(coin);
89+
} catch (error) {
90+
const err = error as Error;
91+
// If URL isn't configured, return undefined instead of throwing
92+
if (err.message.includes('URL not configured')) {
93+
debugLogger('Enclaved express URL not configured, returning undefined');
94+
return undefined;
95+
}
96+
throw err;
97+
}
98+
}

src/masterExpressApp.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import debug from 'debug';
66
import https from 'https';
77
import http from 'http';
88
import superagent from 'superagent';
9+
import { BitGo, BitGoOptions } from 'bitgo';
910

1011
import { MasterExpressConfig, config, isMasterExpressConfig } from './config';
1112
import {
@@ -19,8 +20,22 @@ import {
1920
readCertificates,
2021
setupHealthCheckRoutes,
2122
} from './shared/appUtils';
23+
import { ApiResponseError } from './errors';
24+
import bodyParser from 'body-parser';
25+
import { ProxyAgent } from 'proxy-agent';
26+
import { promiseWrapper } from './routes';
27+
import pjson from '../package.json';
28+
import { createEnclavedExpressClient } from './masterBitgoExpress/enclavedExpressClient';
2229

2330
const debugLogger = debug('master-express:express');
31+
const { version } = require('bitgo/package.json');
32+
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${pjson.version} BitGoJS/${version}`;
33+
34+
// Add this interface before the startup function
35+
interface BitGoRequest extends express.Request {
36+
bitgo: BitGo;
37+
config: MasterExpressConfig;
38+
}
2439

2540
/**
2641
* Create a startup function which will be run upon server initialization
@@ -43,6 +58,75 @@ function isSSL(config: MasterExpressConfig): boolean {
4358
return Boolean((keyPath && crtPath) || (sslKey && sslCert));
4459
}
4560

61+
/**
62+
*
63+
* @param status
64+
* @param result
65+
* @param message
66+
*/
67+
function apiResponse(status: number, result: any, message: string): ApiResponseError {
68+
return new ApiResponseError(message, status, result);
69+
}
70+
71+
const expressJSONParser = bodyParser.json({ limit: '20mb' });
72+
73+
/**
74+
* Perform body parsing here only on routes we want
75+
*/
76+
function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
77+
// Set the default Content-Type, in case the client doesn't set it. If
78+
// Content-Type isn't specified, Express silently refuses to parse the
79+
// request body.
80+
req.headers['content-type'] = req.headers['content-type'] || 'application/json';
81+
return expressJSONParser(req, res, next);
82+
}
83+
84+
/**
85+
* Create the bitgo object in the request
86+
* @param config
87+
*/
88+
function prepareBitGo(config: MasterExpressConfig) {
89+
const { env, customRootUri } = config;
90+
91+
return function prepBitGo(
92+
req: express.Request,
93+
res: express.Response,
94+
next: express.NextFunction,
95+
) {
96+
// Get access token
97+
let accessToken;
98+
if (req.headers.authorization) {
99+
const authSplit = req.headers.authorization.split(' ');
100+
if (authSplit.length === 2 && authSplit[0].toLowerCase() === 'bearer') {
101+
accessToken = authSplit[1];
102+
}
103+
}
104+
const userAgent = req.headers['user-agent']
105+
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
106+
: BITGOEXPRESS_USER_AGENT;
107+
108+
const useProxyUrl = process.env.BITGO_USE_PROXY;
109+
const bitgoConstructorParams: BitGoOptions = {
110+
env,
111+
customRootURI: customRootUri,
112+
accessToken,
113+
userAgent,
114+
...(useProxyUrl
115+
? {
116+
customProxyAgent: new ProxyAgent({
117+
getProxyForUrl: () => useProxyUrl,
118+
}),
119+
}
120+
: {}),
121+
};
122+
123+
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams);
124+
(req as BitGoRequest).config = config;
125+
126+
next();
127+
};
128+
}
129+
46130
async function createHttpsServer(
47131
app: express.Application,
48132
config: MasterExpressConfig,
@@ -98,7 +182,6 @@ function setupMasterExpressRoutes(app: express.Application): void {
98182
// Add enclaved express ping route
99183
app.get('/ping/enclavedExpress', async (req, res) => {
100184
const cfg = config() as MasterExpressConfig;
101-
102185
try {
103186
console.log('Pinging enclaved express');
104187
console.log('SSL Enabled:', cfg.enableSSL);

src/routes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function setupRoutes(app: express.Application): void {
6666
// promiseWrapper implementation
6767
export function promiseWrapper(promiseRequestHandler: any) {
6868
return async function promWrapper(req: any, res: any, next: any) {
69-
debug(`handle: ${req.method} ${req.originalUrl}`);
69+
debugLogger(`handle: ${req.method} ${req.originalUrl}`);
7070
try {
7171
const result = await promiseRequestHandler(req, res, next);
7272
if (typeof result === 'object' && result !== null && 'body' in result && 'status' in result) {

src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,13 @@ export interface MasterExpressConfig extends BaseConfig {
6464

6565
// Union type for the configuration
6666
export type Config = EnclavedConfig | MasterExpressConfig;
67+
68+
// Type guard for MasterExpressConfig
69+
export function isMasterExpressConfig(config: Config): config is MasterExpressConfig {
70+
return config.appMode === AppMode.MASTER_EXPRESS;
71+
}
72+
73+
// Type guard for EnclavedConfig
74+
export function isEnclavedConfig(config: Config): config is EnclavedConfig {
75+
return config.appMode === AppMode.ENCLAVED;
76+
}

0 commit comments

Comments
 (0)