Skip to content

Commit 6de52b8

Browse files
Merge pull request #18 from BitGo/WP-4622/api-ts-mbe
Wp 4622/api ts mbe
2 parents 3582e9d + f98962b commit 6de52b8

File tree

8 files changed

+530
-123
lines changed

8 files changed

+530
-123
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,16 @@
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+
"@api-ts/io-ts-http": "^3.2.1",
21+
"@api-ts/response": "^2.1.0",
22+
"@api-ts/openapi-generator": "^5.7.0",
23+
"@api-ts/typed-express-router": "^1.1.13",
2024
"@bitgo/sdk-core": "^35.2.0",
2125
"bitgo": "^48.0.0",
2226
"body-parser": "^1.20.3",
2327
"connect-timeout": "^1.9.0",
2428
"debug": "^3.1.0",
29+
"io-ts": "2.1.3",
2530
"winston": "^3.11.0",
2631
"express": "4.17.3",
2732
"lodash": "^4.17.20",
@@ -32,6 +37,7 @@
3237
"zod": "^3.25.48"
3338
},
3439
"devDependencies": {
40+
"@api-ts/openapi-generator": "^5.7.0",
3541
"nodemon": "^3.1.10",
3642
"@types/body-parser": "^1.17.0",
3743
"@types/connect-timeout": "^1.9.0",
@@ -60,7 +66,7 @@
6066
"supertest": "^4.0.2",
6167
"ts-jest": "^29.1.2",
6268
"typescript": "^4.2.4"
63-
},
69+
},
6470
"engines": {
6571
"node": ">=22.1.0"
6672
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as t from 'io-ts';
2+
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
3+
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
4+
import { Response } from '@api-ts/response';
5+
import https from 'https';
6+
import superagent from 'superagent';
7+
import { MasterExpressConfig, TlsMode } from '../../config';
8+
import logger from '../../logger';
9+
import { withResponseHandler } from '../../shared/responseHandler';
10+
11+
// Response type for /ping/enclavedExpress endpoint
12+
const PingEnclavedResponse: HttpResponse = {
13+
200: t.type({
14+
status: t.string,
15+
// TODO: Move to common definition between enclavedExpress and masterExpress
16+
enclavedResponse: t.type({
17+
message: t.string,
18+
timestamp: t.string,
19+
}),
20+
}),
21+
500: t.type({
22+
error: t.string,
23+
details: t.string,
24+
}),
25+
};
26+
27+
// API Specification
28+
export const EnclavedExpressApiSpec = apiSpec({
29+
'v1.enclaved.ping': {
30+
post: httpRoute({
31+
method: 'POST',
32+
path: '/ping/enclavedExpress',
33+
request: httpRequest({}),
34+
response: PingEnclavedResponse,
35+
description: 'Ping the enclaved express server',
36+
}),
37+
},
38+
});
39+
40+
// Create router with handlers
41+
export function createEnclavedExpressRouter(
42+
cfg: MasterExpressConfig,
43+
): WrappedRouter<typeof EnclavedExpressApiSpec> {
44+
const router = createRouter(EnclavedExpressApiSpec, {
45+
onDecodeError: (err, _req, _res) => {
46+
logger.error('Decode error:', { error: err });
47+
},
48+
onEncodeError: (err, _req, _res) => {
49+
logger.error('Encode error:', { error: err });
50+
},
51+
});
52+
53+
// Ping endpoint handler
54+
router.post('v1.enclaved.ping', [
55+
withResponseHandler(async () => {
56+
logger.debug('Pinging enclaved express');
57+
58+
try {
59+
let response;
60+
if (cfg.tlsMode === TlsMode.MTLS) {
61+
// Use Master Express's own certificate as client cert when connecting to Enclaved Express
62+
const httpsAgent = new https.Agent({
63+
rejectUnauthorized: !cfg.allowSelfSigned,
64+
ca: cfg.enclavedExpressCert,
65+
// Provide client certificate for mTLS
66+
key: cfg.tlsKey,
67+
cert: cfg.tlsCert,
68+
});
69+
70+
response = await superagent
71+
.post(`${cfg.enclavedExpressUrl}/ping`)
72+
.ca(cfg.enclavedExpressCert)
73+
.agent(httpsAgent)
74+
.send();
75+
} else {
76+
// When TLS is disabled, use plain HTTP without any TLS configuration
77+
response = await superagent.post(`${cfg.enclavedExpressUrl}/ping`).send();
78+
}
79+
80+
return Response.ok({
81+
status: 'Successfully pinged enclaved express',
82+
enclavedResponse: response.body,
83+
});
84+
} catch (error) {
85+
logger.error('Failed to ping enclaved express:', { error });
86+
return Response.internalError({
87+
error: 'Failed to ping enclaved express',
88+
details: error instanceof Error ? error.message : String(error),
89+
});
90+
}
91+
}),
92+
]);
93+
94+
return router;
95+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as t from 'io-ts';
2+
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
3+
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
4+
import { Response } from '@api-ts/response';
5+
import pjson from '../../../package.json';
6+
import { withResponseHandler } from '../../shared/responseHandler';
7+
8+
// Response type for /ping endpoint
9+
const PingResponse: HttpResponse = {
10+
200: t.type({
11+
status: t.string,
12+
timestamp: t.string,
13+
}),
14+
};
15+
16+
// Response type for /version endpoint
17+
const VersionResponse: HttpResponse = {
18+
200: t.type({
19+
version: t.string,
20+
name: t.string,
21+
}),
22+
};
23+
24+
// API Specification
25+
export const HealthCheckApiSpec = apiSpec({
26+
'v1.health.ping': {
27+
post: httpRoute({
28+
method: 'POST',
29+
path: '/ping',
30+
request: httpRequest({}),
31+
response: PingResponse,
32+
description: 'Health check endpoint that returns server status',
33+
}),
34+
},
35+
'v1.health.version': {
36+
get: httpRoute({
37+
method: 'GET',
38+
path: '/version',
39+
request: httpRequest({}),
40+
response: VersionResponse,
41+
description: 'Returns the current version of the server',
42+
}),
43+
},
44+
});
45+
46+
// Create router with handlers
47+
export function createHealthCheckRouter(
48+
serverType: string,
49+
): WrappedRouter<typeof HealthCheckApiSpec> {
50+
const router = createRouter(HealthCheckApiSpec, {
51+
onDecodeError: (_err, _req, _res) => {
52+
console.log(_err);
53+
},
54+
onEncodeError: (_err, _req, _res) => {
55+
console.log(_err);
56+
},
57+
});
58+
59+
// Ping endpoint handler
60+
router.post('v1.health.ping', [
61+
withResponseHandler(() =>
62+
Response.ok({
63+
status: `${serverType} server is ok!`,
64+
timestamp: new Date().toISOString(),
65+
}),
66+
),
67+
]);
68+
69+
// Version endpoint handler
70+
router.get('v1.health.version', [
71+
withResponseHandler(() =>
72+
Response.ok({
73+
version: pjson.version,
74+
name: pjson.name,
75+
}),
76+
),
77+
]);
78+
79+
return router;
80+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import * as t from 'io-ts';
2+
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
3+
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
4+
import { Response } from '@api-ts/response';
5+
import express, { Request } from 'express';
6+
import { BitGo } from 'bitgo';
7+
import { BitGoRequest, isBitGoRequest } from '../../types/request';
8+
import { MasterExpressConfig } from '../../config';
9+
import { handleGenerateWalletOnPrem } from '../generateWallet';
10+
import { withResponseHandler } from '../../shared/responseHandler';
11+
12+
// Middleware functions
13+
export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
14+
req.headers['content-type'] = req.headers['content-type'] || 'application/json';
15+
return express.json({ limit: '20mb' })(req, res, next);
16+
}
17+
18+
export function prepareBitGo(config: MasterExpressConfig) {
19+
const { env, customRootUri } = config;
20+
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${process.env.npm_package_version}`;
21+
22+
return function prepBitGo(
23+
req: express.Request,
24+
res: express.Response,
25+
next: express.NextFunction,
26+
) {
27+
let accessToken;
28+
if (req.headers.authorization) {
29+
const authSplit = req.headers.authorization.split(' ');
30+
if (authSplit.length === 2 && authSplit[0].toLowerCase() === 'bearer') {
31+
accessToken = authSplit[1];
32+
}
33+
}
34+
const userAgent = req.headers['user-agent']
35+
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
36+
: BITGOEXPRESS_USER_AGENT;
37+
38+
const bitgoConstructorParams = {
39+
env,
40+
customRootURI: customRootUri,
41+
accessToken,
42+
userAgent,
43+
};
44+
45+
(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams);
46+
(req as BitGoRequest).config = config;
47+
48+
next();
49+
};
50+
}
51+
52+
// Response type for /generate endpoint
53+
const GenerateWalletResponse: HttpResponse = {
54+
// TODO: Get type from public types repo
55+
200: t.any,
56+
500: t.type({
57+
error: t.string,
58+
details: t.string,
59+
}),
60+
};
61+
62+
// Request type for /generate endpoint
63+
const GenerateWalletRequest = {
64+
label: t.string,
65+
multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]),
66+
enterprise: t.string,
67+
disableTransactionNotifications: t.union([t.undefined, t.boolean]),
68+
isDistributedCustody: t.union([t.undefined, t.boolean]),
69+
};
70+
71+
// API Specification
72+
export const MasterApiSpec = apiSpec({
73+
'v1.wallet.generate': {
74+
post: httpRoute({
75+
method: 'POST',
76+
path: '/{coin}/wallet/generate',
77+
request: httpRequest({
78+
params: {
79+
coin: t.string,
80+
},
81+
body: GenerateWalletRequest,
82+
}),
83+
response: GenerateWalletResponse,
84+
description: 'Generate a new wallet',
85+
}),
86+
},
87+
});
88+
89+
// Create router with handlers
90+
export function createMasterApiRouter(
91+
cfg: MasterExpressConfig,
92+
): WrappedRouter<typeof MasterApiSpec> {
93+
const router = createRouter(MasterApiSpec);
94+
95+
// Add middleware to all routes
96+
router.use(parseBody);
97+
router.use(prepareBitGo(cfg));
98+
99+
// Generate wallet endpoint handler
100+
router.post('v1.wallet.generate', [
101+
withResponseHandler(async (req: BitGoRequest | Request) => {
102+
if (!isBitGoRequest(req)) {
103+
throw new Error('Invalid request type');
104+
}
105+
const result = await handleGenerateWalletOnPrem(req);
106+
return Response.ok(result);
107+
}),
108+
]);
109+
110+
return router;
111+
}

0 commit comments

Comments
 (0)