Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,16 @@
"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'"
},
"dependencies": {
"@api-ts/io-ts-http": "^3.2.1",
"@api-ts/response": "^2.1.0",
"@api-ts/openapi-generator": "^5.7.0",
"@api-ts/typed-express-router": "^1.1.13",
"@bitgo/sdk-core": "^35.2.0",
"bitgo": "^48.0.0",
"body-parser": "^1.20.3",
"connect-timeout": "^1.9.0",
"debug": "^3.1.0",
"io-ts": "2.1.3",
"winston": "^3.11.0",
"express": "4.17.3",
"lodash": "^4.17.20",
Expand All @@ -32,6 +37,7 @@
"zod": "^3.25.48"
},
"devDependencies": {
"@api-ts/openapi-generator": "^5.7.0",
"nodemon": "^3.1.10",
"@types/body-parser": "^1.17.0",
"@types/connect-timeout": "^1.9.0",
Expand Down Expand Up @@ -60,7 +66,7 @@
"supertest": "^4.0.2",
"ts-jest": "^29.1.2",
"typescript": "^4.2.4"
},
},
"engines": {
"node": ">=22.1.0"
}
Expand Down
95 changes: 95 additions & 0 deletions src/masterBitgoExpress/routers/enclavedExpressHealth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as t from 'io-ts';
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
import { Response } from '@api-ts/response';
import https from 'https';
import superagent from 'superagent';
import { MasterExpressConfig, TlsMode } from '../../config';
import logger from '../../logger';
import { withResponseHandler } from '../../shared/responseHandler';

// Response type for /ping/enclavedExpress endpoint
const PingEnclavedResponse: HttpResponse = {
200: t.type({
status: t.string,
// TODO: Move to common definition between enclavedExpress and masterExpress
enclavedResponse: t.type({
message: t.string,
timestamp: t.string,
}),
}),
500: t.type({
error: t.string,
details: t.string,
}),
};

// API Specification
export const EnclavedExpressApiSpec = apiSpec({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const EnclavedExpressApiSpec = apiSpec({
export const MasterExpressApiSpec = apiSpec({

'v1.enclaved.ping': {
post: httpRoute({
method: 'POST',
path: '/ping/enclavedExpress',
request: httpRequest({}),
response: PingEnclavedResponse,
description: 'Ping the enclaved express server',
}),
},
});

// Create router with handlers
export function createEnclavedExpressRouter(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is master express API router

cfg: MasterExpressConfig,
): WrappedRouter<typeof EnclavedExpressApiSpec> {
const router = createRouter(EnclavedExpressApiSpec, {
onDecodeError: (err, _req, _res) => {
logger.error('Decode error:', { error: err });
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onDecodeError handler logs the error but does not send an HTTP response or call next(), leading to a hanging request. Respond with 400 Bad Request or forward the error.

Suggested change
logger.error('Decode error:', { error: err });
logger.error('Decode error:', { error: err });
_res.status(400).json({ error: 'Bad Request', details: err.message });

Copilot uses AI. Check for mistakes.
},
onEncodeError: (err, _req, _res) => {
logger.error('Encode error:', { error: err });
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The onEncodeError handler logs the error but does not send a response or call next(). Consider returning a 500 Internal Server Error response.

Suggested change
logger.error('Encode error:', { error: err });
logger.error('Encode error:', { error: err });
_res.status(500).send({
error: 'Internal Server Error',
details: err instanceof Error ? err.message : String(err),
});

Copilot uses AI. Check for mistakes.
},
});

// Ping endpoint handler
router.post('v1.enclaved.ping', [
withResponseHandler(async () => {
logger.debug('Pinging enclaved express');

try {
let response;
if (cfg.tlsMode === TlsMode.MTLS) {
// Use Master Express's own certificate as client cert when connecting to Enclaved Express
const httpsAgent = new https.Agent({
rejectUnauthorized: !cfg.allowSelfSigned,
ca: cfg.enclavedExpressCert,
// Provide client certificate for mTLS
key: cfg.tlsKey,
cert: cfg.tlsCert,
});

response = await superagent
.post(`${cfg.enclavedExpressUrl}/ping`)
.ca(cfg.enclavedExpressCert)
.agent(httpsAgent)
.send();
} else {
// When TLS is disabled, use plain HTTP without any TLS configuration
response = await superagent.post(`${cfg.enclavedExpressUrl}/ping`).send();
}

return Response.ok({
status: 'Successfully pinged enclaved express',
enclavedResponse: response.body,
});
} catch (error) {
logger.error('Failed to ping enclaved express:', { error });
return Response.internalError({
error: 'Failed to ping enclaved express',
details: error instanceof Error ? error.message : String(error),
});
}
}),
]);

return router;
}
80 changes: 80 additions & 0 deletions src/masterBitgoExpress/routers/healthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as t from 'io-ts';
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
import { Response } from '@api-ts/response';
import pjson from '../../../package.json';
import { withResponseHandler } from '../../shared/responseHandler';

// Response type for /ping endpoint
const PingResponse: HttpResponse = {
200: t.type({
status: t.string,
timestamp: t.string,
}),
};

// Response type for /version endpoint
const VersionResponse: HttpResponse = {
200: t.type({
version: t.string,
name: t.string,
}),
};

// API Specification
export const HealthCheckApiSpec = apiSpec({
'v1.health.ping': {
post: httpRoute({
method: 'POST',
path: '/ping',
request: httpRequest({}),
response: PingResponse,
description: 'Health check endpoint that returns server status',
}),
},
'v1.health.version': {
get: httpRoute({
method: 'GET',
path: '/version',
request: httpRequest({}),
response: VersionResponse,
description: 'Returns the current version of the server',
}),
},
});

// Create router with handlers
export function createHealthCheckRouter(
serverType: string,
): WrappedRouter<typeof HealthCheckApiSpec> {
const router = createRouter(HealthCheckApiSpec, {
onDecodeError: (_err, _req, _res) => {
console.log(_err);
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use structured logging (logger.error) instead of console.log and return a 400 response when request decoding fails.

Copilot uses AI. Check for mistakes.
},
onEncodeError: (_err, _req, _res) => {
console.log(_err);
Copy link

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use structured logging (logger.error) instead of console.log and return a 500 response or call next() when encoding fails.

Copilot uses AI. Check for mistakes.
},
});

// Ping endpoint handler
router.post('v1.health.ping', [
withResponseHandler(() =>
Response.ok({
status: `${serverType} server is ok!`,
timestamp: new Date().toISOString(),
}),
),
]);

// Version endpoint handler
router.get('v1.health.version', [
withResponseHandler(() =>
Response.ok({
version: pjson.version,
name: pjson.name,
}),
),
]);

return router;
}
111 changes: 111 additions & 0 deletions src/masterBitgoExpress/routers/masterApiSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as t from 'io-ts';
import { apiSpec, httpRoute, httpRequest, HttpResponse } from '@api-ts/io-ts-http';
import { createRouter, type WrappedRouter } from '@api-ts/typed-express-router';
import { Response } from '@api-ts/response';
import express, { Request } from 'express';
import { BitGo } from 'bitgo';
import { BitGoRequest, isBitGoRequest } from '../../types/request';
import { MasterExpressConfig } from '../../config';
import { handleGenerateWalletOnPrem } from '../generateWallet';
import { withResponseHandler } from '../../shared/responseHandler';

// Middleware functions
export function parseBody(req: express.Request, res: express.Response, next: express.NextFunction) {
req.headers['content-type'] = req.headers['content-type'] || 'application/json';
return express.json({ limit: '20mb' })(req, res, next);
}

export function prepareBitGo(config: MasterExpressConfig) {
const { env, customRootUri } = config;
const BITGOEXPRESS_USER_AGENT = `BitGoExpress/${process.env.npm_package_version}`;

return function prepBitGo(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
let accessToken;
if (req.headers.authorization) {
const authSplit = req.headers.authorization.split(' ');
if (authSplit.length === 2 && authSplit[0].toLowerCase() === 'bearer') {
accessToken = authSplit[1];
}
}
const userAgent = req.headers['user-agent']
? BITGOEXPRESS_USER_AGENT + ' ' + req.headers['user-agent']
: BITGOEXPRESS_USER_AGENT;

const bitgoConstructorParams = {
env,
customRootURI: customRootUri,
accessToken,
userAgent,
};

(req as BitGoRequest).bitgo = new BitGo(bitgoConstructorParams);
(req as BitGoRequest).config = config;

next();
};
}

// Response type for /generate endpoint
const GenerateWalletResponse: HttpResponse = {
// TODO: Get type from public types repo
200: t.any,
500: t.type({
error: t.string,
details: t.string,
}),
};

// Request type for /generate endpoint
const GenerateWalletRequest = {
label: t.string,
multisigType: t.union([t.undefined, t.literal('onchain'), t.literal('tss')]),
enterprise: t.string,
disableTransactionNotifications: t.union([t.undefined, t.boolean]),
isDistributedCustody: t.union([t.undefined, t.boolean]),
};

// API Specification
export const MasterApiSpec = apiSpec({
'v1.wallet.generate': {
post: httpRoute({
method: 'POST',
path: '/{coin}/wallet/generate',
request: httpRequest({
params: {
coin: t.string,
},
body: GenerateWalletRequest,
}),
response: GenerateWalletResponse,
description: 'Generate a new wallet',
}),
},
});

// Create router with handlers
export function createMasterApiRouter(
cfg: MasterExpressConfig,
): WrappedRouter<typeof MasterApiSpec> {
const router = createRouter(MasterApiSpec);

// Add middleware to all routes
router.use(parseBody);
router.use(prepareBitGo(cfg));

// Generate wallet endpoint handler
router.post('v1.wallet.generate', [
withResponseHandler(async (req: BitGoRequest | Request) => {
if (!isBitGoRequest(req)) {
throw new Error('Invalid request type');
}
const result = await handleGenerateWalletOnPrem(req);
return Response.ok(result);
}),
]);

return router;
}
Loading