diff --git a/README.md b/README.md index 0aaf6b7c..852588ac 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Currently, the following operations are supported: Configuration is done via environment variables: +### Required Settings + +- `APP_MODE` - Application mode (required, must be either "enclaved" or "master-express") + ### Network Settings - `PORT` - Port to listen on (default: 3080) @@ -36,10 +40,20 @@ Configuration is done via environment variables: - `MTLS_REJECT_UNAUTHORIZED` - Whether to reject unauthorized connections (default: false) - `MTLS_ALLOWED_CLIENT_FINGERPRINTS` - Comma-separated list of allowed client certificate fingerprints (optional) +### Master Express Settings + +- `BITGO_PORT` - Port to listen on (default: 3080) +- `BITGO_BIND` - Address to bind to (default: localhost) +- `BITGO_ENV` - Environment name (default: test) +- `BITGO_ENABLE_SSL` - Enable SSL and certificate verification (default: true) +- `BITGO_ENABLE_PROXY` - Enable proxy (default: true) +- `ENCLAVED_EXPRESS_URL` - URL of the enclaved express server (required) +- `ENCLAVED_EXPRESS_SSL_CERT` - Path to the enclaved express server's SSL certificate (required) + ### Other Settings - `LOGFILE` - Path to log file (optional) -- `DEBUG` - Debug namespaces to enable (e.g., 'enclaved:*') +- `DEBUG` - Debug namespaces to enable (e.g., 'enclaved:\*') ## Running Enclaved Express @@ -54,34 +68,44 @@ yarn start --port 3080 For testing purposes, you can use self-signed certificates with relaxed verification: ```bash +APP_MODE=enclaved \ +MASTER_BITGO_EXPRESS_PORT=3080 \ +MASTER_BITGO_EXPRESS_BIND=localhost \ MASTER_BITGO_EXPRESS_KEYPATH=./test-ssl-key.pem \ MASTER_BITGO_EXPRESS_CRTPATH=./test-ssl-cert.pem \ MTLS_ENABLED=true \ MTLS_REQUEST_CERT=true \ MTLS_REJECT_UNAUTHORIZED=false \ -yarn start --port 3080 +yarn start ``` -### Connecting from Regular Express +### Connecting from Master Express -To connect to Enclaved Express from the regular Express server: +To connect to Enclaved Express from the Master Express server: ```bash -yarn start --port 4000 \ - --enclavedExpressUrl='https://localhost:3080' \ - --enclavedExpressSSLCert='./test-ssl-cert.pem' \ - --disableproxy \ - --debug +APP_MODE=master-express \ +BITGO_PORT=3080 \ +BITGO_BIND=localhost \ +BITGO_ENV=test \ +BITGO_KEYPATH=./test-ssl-key.pem \ +BITGO_CRTPATH=./test-ssl-cert.pem \ +ENCLAVED_EXPRESS_URL=https://localhost:4000 \ +ENCLAVED_EXPRESS_SSL_CERT=./enclaved-express-cert.pem \ +BITGO_ENABLE_SSL=false \ +yarn start ``` ## Understanding mTLS Configuration ### Server Side (Enclaved Express) + - Uses both certificate and key files - The key file (`test-ssl-key.pem`) is used to prove the server's identity - The certificate file (`test-ssl-cert.pem`) is what the server presents to clients ### Client Side (Regular Express) + - For testing, only needs the server's certificate - `rejectUnauthorized: false` allows testing without strict certificate verification - In production, proper client certificates should be used @@ -101,11 +125,13 @@ yarn start --port 4000 \ ### Common Issues 1. **Certificate Errors** + - Ensure paths to certificate files are correct - Check file permissions on certificate files - Verify certificate format is correct 2. **Connection Issues** + - Verify ports are not in use - Check firewall settings - Ensure URLs are correct (including https:// prefix) @@ -117,4 +143,4 @@ yarn start --port 4000 \ ## License -MIT \ No newline at end of file +MIT diff --git a/bin/enclaved-bitgo-express b/bin/enclaved-bitgo-express index 5162c122..583e8467 100755 --- a/bin/enclaved-bitgo-express +++ b/bin/enclaved-bitgo-express @@ -8,7 +8,7 @@ process.on('unhandledRejection', (reason, promise) => { console.error(reason); }); -const { init } = require('../dist/src/enclavedApp'); +const { init } = require('../dist/src/app'); if (require.main === module) { init().catch((err) => { diff --git a/package.json b/package.json index 4666437e..aded90aa 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "debug": "^3.1.0", "express": "4.17.3", "lodash": "^4.17.20", - "morgan": "^1.9.1" + "morgan": "^1.9.1", + "superagent": "^8.0.9" }, "devDependencies": { "@types/body-parser": "^1.17.0", diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 8a3dfb4e..4d43c716 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -1,4 +1,4 @@ -import { config, TlsMode } from '../config'; +import { config, isEnclavedConfig, TlsMode } from '../config'; describe('Configuration', () => { const originalEnv = process.env; @@ -15,46 +15,76 @@ describe('Configuration', () => { process.env = originalEnv; }); - it('should use default configuration when no environment variables are set', () => { - const cfg = config(); - expect(cfg.port).toBe(3080); - expect(cfg.bind).toBe('localhost'); - expect(cfg.tlsMode).toBe(TlsMode.ENABLED); - expect(cfg.timeout).toBe(305 * 1000); + it('should throw error when APP_MODE is not set', () => { + expect(() => config()).toThrow('APP_MODE environment variable is required'); }); - it('should read port from environment variable', () => { - process.env.MASTER_BITGO_EXPRESS_PORT = '4000'; - const cfg = config(); - expect(cfg.port).toBe(4000); + it('should throw error when APP_MODE is invalid', () => { + process.env.APP_MODE = 'invalid'; + expect(() => config()).toThrow('Invalid APP_MODE: invalid'); }); - it('should read TLS mode from environment variables', () => { - process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'true'; - let cfg = config(); - expect(cfg.tlsMode).toBe(TlsMode.DISABLED); + describe('Enclaved Mode', () => { + beforeEach(() => { + process.env.APP_MODE = 'enclaved'; + }); - process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'false'; - process.env.MTLS_ENABLED = 'true'; - cfg = config(); - expect(cfg.tlsMode).toBe(TlsMode.MTLS); - }); + it('should use default configuration when no environment variables are set', () => { + const cfg = config(); + expect(isEnclavedConfig(cfg)).toBe(true); + if (isEnclavedConfig(cfg)) { + expect(cfg.port).toBe(3080); + expect(cfg.bind).toBe('localhost'); + expect(cfg.tlsMode).toBe(TlsMode.ENABLED); + expect(cfg.timeout).toBe(305 * 1000); + } + }); - it('should throw error when both TLS disabled and mTLS enabled', () => { - process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'true'; - process.env.MTLS_ENABLED = 'true'; - expect(() => config()).toThrow('Cannot have both TLS disabled and mTLS enabled'); - }); + it('should read port from environment variable', () => { + process.env.MASTER_BITGO_EXPRESS_PORT = '4000'; + const cfg = config(); + expect(isEnclavedConfig(cfg)).toBe(true); + if (isEnclavedConfig(cfg)) { + expect(cfg.port).toBe(4000); + } + }); + + it('should read TLS mode from environment variables', () => { + process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'true'; + let cfg = config(); + expect(isEnclavedConfig(cfg)).toBe(true); + if (isEnclavedConfig(cfg)) { + expect(cfg.tlsMode).toBe(TlsMode.DISABLED); + } + + process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'false'; + process.env.MTLS_ENABLED = 'true'; + cfg = config(); + expect(isEnclavedConfig(cfg)).toBe(true); + if (isEnclavedConfig(cfg)) { + expect(cfg.tlsMode).toBe(TlsMode.MTLS); + } + }); + + it('should throw error when both TLS disabled and mTLS enabled', () => { + process.env.MASTER_BITGO_EXPRESS_DISABLE_TLS = 'true'; + process.env.MTLS_ENABLED = 'true'; + expect(() => config()).toThrow('Cannot have both TLS disabled and mTLS enabled'); + }); - it('should read mTLS settings from environment variables', () => { - process.env.MTLS_ENABLED = 'true'; - process.env.MTLS_REQUEST_CERT = 'true'; - process.env.MTLS_REJECT_UNAUTHORIZED = 'true'; - process.env.MTLS_ALLOWED_CLIENT_FINGERPRINTS = 'ABC123,DEF456'; + it('should read mTLS settings from environment variables', () => { + process.env.MTLS_ENABLED = 'true'; + process.env.MTLS_REQUEST_CERT = 'true'; + process.env.MTLS_REJECT_UNAUTHORIZED = 'true'; + process.env.MTLS_ALLOWED_CLIENT_FINGERPRINTS = 'ABC123,DEF456'; - const cfg = config(); - expect(cfg.mtlsRequestCert).toBe(true); - expect(cfg.mtlsRejectUnauthorized).toBe(true); - expect(cfg.mtlsAllowedClientFingerprints).toEqual(['ABC123', 'DEF456']); + const cfg = config(); + expect(isEnclavedConfig(cfg)).toBe(true); + if (isEnclavedConfig(cfg)) { + expect(cfg.mtlsRequestCert).toBe(true); + expect(cfg.mtlsRejectUnauthorized).toBe(true); + expect(cfg.mtlsAllowedClientFingerprints).toEqual(['ABC123', 'DEF456']); + } + }); }); }); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 00000000..99feb6bc --- /dev/null +++ b/src/app.ts @@ -0,0 +1,26 @@ +/** + * @prettier + */ +import { config, isEnclavedConfig, isMasterExpressConfig } from './config'; +import * as enclavedApp from './enclavedApp'; +import * as masterExpressApp from './masterExpressApp'; + +/** + * Main application entry point that determines the mode and starts the appropriate app + */ +export async function init(): Promise { + const cfg = config(); + + if (isEnclavedConfig(cfg)) { + console.log('Starting in Enclaved mode...'); + await enclavedApp.init(); + } else if (isMasterExpressConfig(cfg)) { + console.log('Starting in Master Express mode...'); + await masterExpressApp.init(); + } else { + throw new Error(`Unknown app mode: ${(cfg as any).appMode}`); + } +} + +// Export the individual app modules for direct access if needed +export { enclavedApp, masterExpressApp }; diff --git a/src/config.ts b/src/config.ts index a5c49c31..0b9f2bd3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,26 +2,58 @@ * @prettier */ import fs from 'fs'; -import { Config, TlsMode } from './types'; +import { + Config, + EnclavedConfig, + MasterExpressConfig, + TlsMode, + AppMode, + EnvironmentName, +} from './types'; -export { Config, TlsMode }; +export { Config, EnclavedConfig, MasterExpressConfig, TlsMode, AppMode, EnvironmentName }; -export const defaultConfig: Config = { +function isNilOrNaN(val: unknown): val is null | undefined | number { + return val == null || (typeof val === 'number' && isNaN(val)); +} + +function readEnvVar(name: string): string | undefined { + if (process.env[name] !== undefined && process.env[name] !== '') { + return process.env[name]; + } +} + +function determineAppMode(): AppMode { + const mode = readEnvVar('APP_MODE') || readEnvVar('BITGO_APP_MODE'); + if (!mode) { + throw new Error( + 'APP_MODE environment variable is required. Set APP_MODE to either "enclaved" or "master-express"', + ); + } + if (mode === 'master-express') { + return AppMode.MASTER_EXPRESS; + } + if (mode === 'enclaved') { + return AppMode.ENCLAVED; + } + throw new Error(`Invalid APP_MODE: ${mode}. Must be either "enclaved" or "master-express"`); +} + +// ============================================================================ +// ENCLAVED MODE CONFIGURATION +// ============================================================================ + +const defaultEnclavedConfig: EnclavedConfig = { + appMode: AppMode.ENCLAVED, port: 3080, bind: 'localhost', timeout: 305 * 1000, logFile: '', - tlsMode: TlsMode.ENABLED, // Default to TLS enabled + tlsMode: TlsMode.ENABLED, mtlsRequestCert: false, mtlsRejectUnauthorized: false, }; -function readEnvVar(name: string): string | undefined { - if (process.env[name] !== undefined && process.env[name] !== '') { - return process.env[name]; - } -} - function determineTlsMode(): TlsMode { const disableTls = readEnvVar('MASTER_BITGO_EXPRESS_DISABLE_TLS') === 'true'; const mtlsEnabled = readEnvVar('MTLS_ENABLED') === 'true'; @@ -30,58 +62,255 @@ function determineTlsMode(): TlsMode { throw new Error('Cannot have both TLS disabled and mTLS enabled'); } - if (disableTls) { - return TlsMode.DISABLED; - } - if (mtlsEnabled) { - return TlsMode.MTLS; - } + if (disableTls) return TlsMode.DISABLED; + if (mtlsEnabled) return TlsMode.MTLS; return TlsMode.ENABLED; } -export function config(): Config { - const envConfig: Partial = { - port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')) || defaultConfig.port, - bind: readEnvVar('MASTER_BITGO_EXPRESS_BIND') || defaultConfig.bind, +function enclavedEnvConfig(): Partial { + return { + appMode: AppMode.ENCLAVED, + port: Number(readEnvVar('MASTER_BITGO_EXPRESS_PORT')), + bind: readEnvVar('MASTER_BITGO_EXPRESS_BIND'), ipc: readEnvVar('MASTER_BITGO_EXPRESS_IPC'), debugNamespace: (readEnvVar('MASTER_BITGO_EXPRESS_DEBUG_NAMESPACE') || '') .split(',') .filter(Boolean), - // Basic TLS settings from MASTER_BITGO_EXPRESS + logFile: readEnvVar('MASTER_BITGO_EXPRESS_LOGFILE'), + timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')), + keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')), + headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')), + // TLS settings keyPath: readEnvVar('MASTER_BITGO_EXPRESS_KEYPATH'), crtPath: readEnvVar('MASTER_BITGO_EXPRESS_CRTPATH'), tlsKey: readEnvVar('MASTER_BITGO_EXPRESS_TLS_KEY'), tlsCert: readEnvVar('MASTER_BITGO_EXPRESS_TLS_CERT'), - // Determine TLS mode tlsMode: determineTlsMode(), // mTLS settings mtlsRequestCert: readEnvVar('MTLS_REQUEST_CERT') === 'true', mtlsRejectUnauthorized: readEnvVar('MTLS_REJECT_UNAUTHORIZED') === 'true', mtlsAllowedClientFingerprints: readEnvVar('MTLS_ALLOWED_CLIENT_FINGERPRINTS')?.split(','), - // Other settings - logFile: readEnvVar('MASTER_BITGO_EXPRESS_LOGFILE'), - timeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_TIMEOUT')) || defaultConfig.timeout, - keepAliveTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_KEEP_ALIVE_TIMEOUT')), - headersTimeout: Number(readEnvVar('MASTER_BITGO_EXPRESS_HEADERS_TIMEOUT')), }; +} + +function mergeEnclavedConfigs(...configs: Partial[]): EnclavedConfig { + function get(k: T): EnclavedConfig[T] { + return configs.reduce( + (entry: EnclavedConfig[T], config) => + !isNilOrNaN(config[k]) ? (config[k] as EnclavedConfig[T]) : entry, + defaultEnclavedConfig[k], + ); + } + + return { + appMode: AppMode.ENCLAVED, + port: get('port'), + bind: get('bind'), + ipc: get('ipc'), + debugNamespace: get('debugNamespace'), + logFile: get('logFile'), + timeout: get('timeout'), + keepAliveTimeout: get('keepAliveTimeout'), + headersTimeout: get('headersTimeout'), + keyPath: get('keyPath'), + crtPath: get('crtPath'), + tlsKey: get('tlsKey'), + tlsCert: get('tlsCert'), + tlsMode: get('tlsMode'), + mtlsRequestCert: get('mtlsRequestCert'), + mtlsRejectUnauthorized: get('mtlsRejectUnauthorized'), + mtlsAllowedClientFingerprints: get('mtlsAllowedClientFingerprints'), + }; +} + +function configureEnclavedMode(): EnclavedConfig { + const env = enclavedEnvConfig(); + let config = mergeEnclavedConfigs(env); - // Support loading key/cert from file if keyPath/crtPath are set and tlsKey/tlsCert are not - if (!envConfig.tlsKey && envConfig.keyPath) { + // Handle file loading for TLS certificates + if (!config.tlsKey && config.keyPath) { try { - envConfig.tlsKey = fs.readFileSync(envConfig.keyPath, 'utf-8'); + config = { ...config, tlsKey: fs.readFileSync(config.keyPath, 'utf-8') }; } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); throw new Error(`Failed to read TLS key from keyPath: ${err.message}`); } } - if (!envConfig.tlsCert && envConfig.crtPath) { + if (!config.tlsCert && config.crtPath) { try { - envConfig.tlsCert = fs.readFileSync(envConfig.crtPath, 'utf-8'); + config = { ...config, tlsCert: fs.readFileSync(config.crtPath, 'utf-8') }; } catch (e) { const err = e instanceof Error ? e : new Error(String(e)); throw new Error(`Failed to read TLS certificate from crtPath: ${err.message}`); } } - return { ...defaultConfig, ...envConfig }; + return config; +} + +// ============================================================================ +// MASTER EXPRESS MODE CONFIGURATION +// ============================================================================ + +const defaultMasterExpressConfig: MasterExpressConfig = { + appMode: AppMode.MASTER_EXPRESS, + port: 3080, + bind: 'localhost', + timeout: 305 * 1000, + logFile: '', + env: 'test', + enableSSL: true, + enableProxy: true, + disableEnvCheck: true, + authVersion: 2, + enclavedExpressUrl: '', // Will be overridden by environment variable + enclavedExpressSSLCert: '', // Will be overridden by environment variable +}; + +function forceSecureUrl(url: string): string { + const regex = new RegExp(/(^\w+:|^)\/\//); + if (regex.test(url)) { + return url.replace(/(^\w+:|^)\/\//, 'https://'); + } + return `https://${url}`; +} + +function masterExpressEnvConfig(): Partial { + const enclavedExpressUrl = readEnvVar('ENCLAVED_EXPRESS_URL'); + const enclavedExpressSSLCert = readEnvVar('ENCLAVED_EXPRESS_SSL_CERT'); + + if (!enclavedExpressUrl) { + throw new Error('ENCLAVED_EXPRESS_URL environment variable is required and cannot be empty'); + } + + if (!enclavedExpressSSLCert) { + throw new Error( + 'ENCLAVED_EXPRESS_SSL_CERT environment variable is required and cannot be empty', + ); + } + + return { + appMode: AppMode.MASTER_EXPRESS, + port: Number(readEnvVar('BITGO_PORT')), + bind: readEnvVar('BITGO_BIND'), + ipc: readEnvVar('BITGO_IPC'), + debugNamespace: (readEnvVar('BITGO_DEBUG_NAMESPACE') || '').split(',').filter(Boolean), + logFile: readEnvVar('BITGO_LOGFILE'), + timeout: Number(readEnvVar('BITGO_TIMEOUT')), + keepAliveTimeout: Number(readEnvVar('BITGO_KEEP_ALIVE_TIMEOUT')), + headersTimeout: Number(readEnvVar('BITGO_HEADERS_TIMEOUT')), + // BitGo API settings + env: readEnvVar('BITGO_ENV') as EnvironmentName, + customRootUri: readEnvVar('BITGO_CUSTOM_ROOT_URI'), + enableSSL: readEnvVar('BITGO_ENABLE_SSL') !== 'false', // Default to true unless explicitly set to false + enableProxy: readEnvVar('BITGO_ENABLE_PROXY') !== 'false', // Default to true unless explicitly set to false + disableEnvCheck: readEnvVar('BITGO_DISABLE_ENV_CHECK') === 'true', + authVersion: Number(readEnvVar('BITGO_AUTH_VERSION')), + enclavedExpressUrl, + enclavedExpressSSLCert, + customBitcoinNetwork: readEnvVar('BITGO_CUSTOM_BITCOIN_NETWORK'), + // SSL settings + keyPath: readEnvVar('BITGO_KEYPATH'), + crtPath: readEnvVar('BITGO_CRTPATH'), + sslKey: readEnvVar('BITGO_SSL_KEY'), + sslCert: readEnvVar('BITGO_SSL_CERT'), + }; +} + +function mergeMasterExpressConfigs( + ...configs: Partial[] +): MasterExpressConfig { + function get(k: T): MasterExpressConfig[T] { + return configs.reduce( + (entry: MasterExpressConfig[T], config) => + !isNilOrNaN(config[k]) ? (config[k] as MasterExpressConfig[T]) : entry, + defaultMasterExpressConfig[k], + ); + } + + return { + appMode: AppMode.MASTER_EXPRESS, + port: get('port'), + bind: get('bind'), + ipc: get('ipc'), + debugNamespace: get('debugNamespace'), + logFile: get('logFile'), + timeout: get('timeout'), + keepAliveTimeout: get('keepAliveTimeout'), + headersTimeout: get('headersTimeout'), + env: get('env'), + customRootUri: get('customRootUri'), + enableSSL: get('enableSSL'), + enableProxy: get('enableProxy'), + disableEnvCheck: get('disableEnvCheck'), + authVersion: get('authVersion'), + enclavedExpressUrl: get('enclavedExpressUrl'), + enclavedExpressSSLCert: get('enclavedExpressSSLCert'), + customBitcoinNetwork: get('customBitcoinNetwork'), + keyPath: get('keyPath'), + crtPath: get('crtPath'), + sslKey: get('sslKey'), + sslCert: get('sslCert'), + }; +} + +function configureMasterExpressMode(): MasterExpressConfig { + const env = masterExpressEnvConfig(); + let config = mergeMasterExpressConfigs(env); + + // Post-process URLs if SSL is enabled + if (config.enableSSL) { + const updates: Partial = {}; + if (config.customRootUri) { + updates.customRootUri = forceSecureUrl(config.customRootUri); + } + if (config.enclavedExpressUrl) { + updates.enclavedExpressUrl = forceSecureUrl(config.enclavedExpressUrl); + } + config = { ...config, ...updates }; + } + + // Handle SSL cert loading + if (config.enclavedExpressSSLCert) { + try { + if (fs.existsSync(config.enclavedExpressSSLCert)) { + config = { + ...config, + enclavedExpressSSLCert: fs.readFileSync(config.enclavedExpressSSLCert, 'utf-8'), + }; + } else { + throw new Error(`Certificate file not found: ${config.enclavedExpressSSLCert}`); + } + } catch (e) { + const err = e instanceof Error ? e : new Error(String(e)); + throw new Error(`Failed to read enclaved express SSL cert: ${err.message}`); + } + } + + return config; +} + +// ============================================================================ +// MAIN CONFIG FUNCTION +// ============================================================================ + +export function config(): Config { + const appMode = determineAppMode(); + + if (appMode === AppMode.ENCLAVED) { + return configureEnclavedMode(); + } else if (appMode === AppMode.MASTER_EXPRESS) { + return configureMasterExpressMode(); + } else { + throw new Error(`Unknown app mode: ${appMode}`); + } +} + +// Type guards for working with the union type +export function isEnclavedConfig(config: Config): config is EnclavedConfig { + return config.appMode === AppMode.ENCLAVED; +} + +export function isMasterExpressConfig(config: Config): config is MasterExpressConfig { + return config.appMode === AppMode.MASTER_EXPRESS; } diff --git a/src/enclavedApp.ts b/src/enclavedApp.ts index 1b2d6a02..21d31edc 100644 --- a/src/enclavedApp.ts +++ b/src/enclavedApp.ts @@ -2,61 +2,34 @@ * @prettier */ import express from 'express'; -import path from 'path'; import debug from 'debug'; import https from 'https'; import http from 'http'; import morgan from 'morgan'; -import fs from 'fs'; -import timeout from 'connect-timeout'; -import bodyParser from 'body-parser'; -import _ from 'lodash'; import { SSL_OP_NO_TLSv1 } from 'constants'; -import pjson from '../package.json'; -import { Config, config, TlsMode } from './config'; +import { EnclavedConfig, config, TlsMode, isEnclavedConfig } from './config'; import * as routes from './routes'; +import { + setupLogging, + setupDebugNamespaces, + setupCommonMiddleware, + createErrorHandler, + createHttpServer, + configureServerTimeouts, + prepareIpc, + readCertificates, +} from './shared/appUtils'; const debugLogger = debug('enclaved:express'); -/** - * Set up the logging middleware provided by morgan - * - * @param app - * @param config - */ -function setupLogging(app: express.Application, config: Config): void { - // Set up morgan for logging, with optional logging into a file - let middleware; - if (config.logFile) { - // create a write stream (in append mode) - const accessLogPath = path.resolve(config.logFile); - const accessLogStream = fs.createWriteStream(accessLogPath, { flags: 'a' }); - /* eslint-disable-next-line no-console */ - console.log('Log location: ' + accessLogPath); - // setup the logger - middleware = morgan('combined', { stream: accessLogStream }); - } else { - middleware = morgan('combined'); - } - - app.use(middleware); - morgan.token('remote-user', function (req: express.Request) { - return (req as any).clientCert ? (req as any).clientCert.subject.CN : 'unknown'; - }); -} - /** * Create a startup function which will be run upon server initialization - * - * @param config - * @param baseUri - * @return {Function} */ -export function startup(config: Config, baseUri: string): () => void { +export function startup(config: EnclavedConfig, baseUri: string): () => void { return function () { /* eslint-disable no-console */ - console.log('BitGo-enclaved-bitgo-express running'); + console.log('BitGo Enclaved Express running'); console.log(`Base URI: ${baseUri}`); console.log(`TLS Mode: ${config.tlsMode}`); console.log(`mTLS Enabled: ${config.tlsMode === TlsMode.MTLS}`); @@ -66,7 +39,7 @@ export function startup(config: Config, baseUri: string): () => void { }; } -function isTLS(config: Config): boolean { +function isTLS(config: EnclavedConfig): boolean { const { keyPath, crtPath, tlsKey, tlsCert, tlsMode } = config; console.log('TLS Configuration:', { tlsMode, @@ -79,19 +52,23 @@ function isTLS(config: Config): boolean { return Boolean((keyPath && crtPath) || (tlsKey && tlsCert)); } -async function createHttpsServer(app: express.Application, config: Config): Promise { +async function createHttpsServer( + app: express.Application, + config: EnclavedConfig, +): Promise { const { keyPath, crtPath, tlsKey, tlsCert, tlsMode, mtlsRequestCert, mtlsRejectUnauthorized } = config; let key: string; let cert: string; + if (tlsKey && tlsCert) { key = tlsKey; cert = tlsCert; console.log('Using TLS key and cert from environment variables'); } else if (keyPath && crtPath) { - const privateKeyPromise = fs.promises.readFile(keyPath, 'utf8'); - const certificatePromise = fs.promises.readFile(crtPath, 'utf8'); - [key, cert] = await Promise.all([privateKeyPromise, certificatePromise]); + const certificates = await readCertificates(keyPath, crtPath); + key = certificates.key; + cert = certificates.cert; console.log(`Using TLS key and cert from files: ${keyPath}, ${crtPath}`); } else { throw new Error('Failed to get TLS key and certificate'); @@ -130,57 +107,26 @@ async function createHttpsServer(app: express.Application, config: Config): Prom return server; } -function createHttpServer(app: express.Application): http.Server { - return http.createServer(app); -} - export async function createServer( - config: Config, + config: EnclavedConfig, app: express.Application, ): Promise { const server = isTLS(config) ? await createHttpsServer(app, config) : createHttpServer(app); - if (config.keepAliveTimeout !== undefined) { - server.keepAliveTimeout = config.keepAliveTimeout; - } - if (config.headersTimeout !== undefined) { - server.headersTimeout = config.headersTimeout; - } + configureServerTimeouts(server, config); return server; } -export function createBaseUri(config: Config): string { +export function createBaseUri(config: EnclavedConfig): string { const { bind, port } = config; const tls = isTLS(config); const isStandardPort = (port === 80 && !tls) || (port === 443 && tls); return `http${tls ? 's' : ''}://${bind}${!isStandardPort ? ':' + port : ''}`; } -/** - * Create error handling middleware - */ -function errorHandler() { - return function ( - err: any, - req: express.Request, - res: express.Response, - _next: express.NextFunction, - ) { - debugLogger('Error: ' + (err && err.message ? err.message : String(err))); - const statusCode = err && err.status ? err.status : 500; - const result = { - error: err && err.message ? err.message : String(err), - name: err && err.name ? err.name : 'Error', - code: err && err.code ? err.code : undefined, - version: pjson.version, - }; - return res.status(statusCode).json(result); - }; -} - /** * Create and configure the express application */ -export function app(cfg: Config): express.Application { +export function app(cfg: EnclavedConfig): express.Application { debugLogger('app is initializing'); const app = express(); @@ -188,62 +134,33 @@ export function app(cfg: Config): express.Application { setupLogging(app, cfg); debugLogger('logging setup'); - const { debugNamespace } = cfg; - - // enable specified debug namespaces - if (_.isArray(debugNamespace)) { - for (const ns of debugNamespace) { - if (ns && !debug.enabled(ns)) { - debug.enable(ns); - } - } - } - - // Be more robust about accepting URLs with double slashes - app.use(function replaceUrlSlashes( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) { - req.url = req.url.replace(/\/{2,}/g, '/'); - next(); + // Add custom morgan token for mTLS client certificate + morgan.token('remote-user', function (req: express.Request) { + return (req as any).clientCert ? (req as any).clientCert.subject.CN : 'unknown'; }); - // Set timeout - app.use(timeout(cfg.timeout) as any); - - // Add body parser - app.use(bodyParser.json({ limit: '20mb' })); + setupDebugNamespaces(cfg.debugNamespace); + setupCommonMiddleware(app, cfg); // Setup routes routes.setupRoutes(app); // Add error handler - app.use(errorHandler()); + app.use(createErrorHandler(debugLogger)); return app; } -// Add prepareIpc function -async function prepareIpc(ipcSocketFilePath: string) { - if (process.platform === 'win32') { - throw new Error(`IPC option is not supported on platform ${process.platform}`); - } - try { - const stat = fs.statSync(ipcSocketFilePath); - if (!stat.isSocket()) { - throw new Error('IPC socket is not actually a socket'); - } - fs.unlinkSync(ipcSocketFilePath); - } catch (e: any) { - if (e.code !== 'ENOENT') { - throw e; - } - } -} - export async function init(): Promise { const cfg = config(); + + // Type-safe validation that we're in enclaved mode + if (!isEnclavedConfig(cfg)) { + throw new Error( + `This application only supports enclaved mode. Current mode: ${cfg.appMode}. Set APP_MODE=enclaved to use this application.`, + ); + } + const expressApp = app(cfg); const server = await createServer(cfg, expressApp); const { port, bind, ipc } = cfg; diff --git a/src/masterExpressApp.ts b/src/masterExpressApp.ts new file mode 100644 index 00000000..05f6077f --- /dev/null +++ b/src/masterExpressApp.ts @@ -0,0 +1,187 @@ +/** + * @prettier + */ +import express from 'express'; +import debug from 'debug'; +import https from 'https'; +import http from 'http'; +import superagent from 'superagent'; + +import { MasterExpressConfig, config, isMasterExpressConfig } from './config'; +import { + setupLogging, + setupDebugNamespaces, + setupCommonMiddleware, + createErrorHandler, + createHttpServer, + configureServerTimeouts, + prepareIpc, + readCertificates, + setupHealthCheckRoutes, +} from './shared/appUtils'; + +const debugLogger = debug('master-express:express'); + +/** + * Create a startup function which will be run upon server initialization + */ +export function startup(config: MasterExpressConfig, baseUri: string): () => void { + return function () { + /* eslint-disable no-console */ + console.log('BitGo Master Express running'); + console.log(`Base URI: ${baseUri}`); + console.log(`Environment: ${config.env}`); + console.log(`SSL Enabled: ${config.enableSSL}`); + console.log(`Proxy Enabled: ${config.enableProxy}`); + /* eslint-enable no-console */ + }; +} + +function isSSL(config: MasterExpressConfig): boolean { + const { keyPath, crtPath, sslKey, sslCert } = config; + if (!config.enableSSL) return false; + return Boolean((keyPath && crtPath) || (sslKey && sslCert)); +} + +async function createHttpsServer( + app: express.Application, + config: MasterExpressConfig, +): Promise { + const { keyPath, crtPath, sslKey, sslCert } = config; + let key: string; + let cert: string; + + if (sslKey && sslCert) { + key = sslKey; + cert = sslCert; + console.log('Using SSL key and cert from environment variables'); + } else if (keyPath && crtPath) { + const certificates = await readCertificates(keyPath, crtPath); + key = certificates.key; + cert = certificates.cert; + console.log(`Using SSL key and cert from files: ${keyPath}, ${crtPath}`); + } else { + throw new Error('Failed to get SSL key and certificate'); + } + + const httpsOptions: https.ServerOptions = { + key, + cert, + }; + + return https.createServer(httpsOptions, app); +} + +export async function createServer( + config: MasterExpressConfig, + app: express.Application, +): Promise { + const server = isSSL(config) ? await createHttpsServer(app, config) : createHttpServer(app); + configureServerTimeouts(server, config); + return server; +} + +export function createBaseUri(config: MasterExpressConfig): string { + const { bind, port } = config; + const ssl = isSSL(config); + const isStandardPort = (port === 80 && !ssl) || (port === 443 && ssl); + return `http${ssl ? 's' : ''}://${bind}${!isStandardPort ? ':' + port : ''}`; +} + +/** + * Setup master express specific routes + */ +function setupMasterExpressRoutes(app: express.Application): void { + // Setup common health check routes + setupHealthCheckRoutes(app, 'master express'); + + // Add enclaved express ping route + app.get('/ping/enclavedExpress', async (req, res) => { + const cfg = config() as MasterExpressConfig; + + try { + console.log('Pinging enclaved express'); + console.log('SSL Enabled:', cfg.enableSSL); + console.log('Enclaved Express URL:', cfg.enclavedExpressUrl); + console.log('Certificate exists:', Boolean(cfg.enclavedExpressSSLCert)); + console.log('Certificate length:', cfg.enclavedExpressSSLCert.length); + console.log('Certificate content:', cfg.enclavedExpressSSLCert); + const response = await superagent + .get(`${cfg.enclavedExpressUrl}/ping`) + .ca(cfg.enclavedExpressSSLCert) + .agent( + new https.Agent({ + rejectUnauthorized: cfg.enableSSL, + ca: cfg.enclavedExpressSSLCert, + }), + ) + .send(); + + res.json({ + status: 'Successfully pinged enclaved express', + enclavedResponse: response.body, + }); + } catch (error) { + debugLogger('Failed to ping enclaved express:', error); + res.status(500).json({ + error: 'Failed to ping enclaved express', + details: error instanceof Error ? error.message : String(error), + }); + } + }); + + // Add a catch-all for unsupported routes + app.use('*', (_req, res) => { + res.status(404).json({ + error: 'Route not found or not supported in master express mode', + }); + }); + + debugLogger('Master express routes configured'); +} + +/** + * Create and configure the express application for master express mode + */ +export function app(cfg: MasterExpressConfig): express.Application { + debugLogger('master express app is initializing'); + + const app = express(); + + setupLogging(app, cfg); + debugLogger('logging setup'); + + setupDebugNamespaces(cfg.debugNamespace); + setupCommonMiddleware(app, cfg); + + // Setup master express routes + setupMasterExpressRoutes(app); + + // Add error handler + app.use(createErrorHandler(debugLogger)); + + return app; +} + +export async function init(): Promise { + const cfg = config(); + + // Type-safe validation that we're in master express mode + if (!isMasterExpressConfig(cfg)) { + throw new Error( + `This application only supports master express mode. Current mode: ${cfg.appMode}. Set APP_MODE=master-express to use this application.`, + ); + } + + const expressApp = app(cfg); + const server = await createServer(cfg, expressApp); + const { port, bind, ipc } = cfg; + const baseUri = createBaseUri(cfg); + + if (ipc) { + await prepareIpc(ipc); + server.listen(ipc, startup(cfg, baseUri)); + } else { + server.listen(port, bind, startup(cfg, baseUri)); + } +} diff --git a/src/shared/appUtils.ts b/src/shared/appUtils.ts new file mode 100644 index 00000000..8699eed0 --- /dev/null +++ b/src/shared/appUtils.ts @@ -0,0 +1,164 @@ +/** + * @prettier + */ +import express from 'express'; +import path from 'path'; +import debug from 'debug'; +import https from 'https'; +import http from 'http'; +import morgan from 'morgan'; +import fs from 'fs'; +import timeout from 'connect-timeout'; +import bodyParser from 'body-parser'; +import _ from 'lodash'; +import pjson from '../../package.json'; + +import { Config } from '../config'; + +/** + * Set up the logging middleware provided by morgan + */ +export function setupLogging(app: express.Application, config: Config): void { + // Set up morgan for logging, with optional logging into a file + let middleware; + if (config.logFile) { + // create a write stream (in append mode) + const accessLogPath = path.resolve(config.logFile); + const accessLogStream = fs.createWriteStream(accessLogPath, { flags: 'a' }); + /* eslint-disable-next-line no-console */ + console.log('Log location: ' + accessLogPath); + // setup the logger + middleware = morgan('combined', { stream: accessLogStream }); + } else { + middleware = morgan('combined'); + } + + app.use(middleware); +} + +/** + * Setup debug namespaces + */ +export function setupDebugNamespaces(debugNamespace?: string[]): void { + if (_.isArray(debugNamespace)) { + for (const ns of debugNamespace) { + if (ns && !debug.enabled(ns)) { + debug.enable(ns); + } + } + } +} + +/** + * Create common Express middleware + */ +export function setupCommonMiddleware(app: express.Application, config: Config): void { + // Be more robust about accepting URLs with double slashes + app.use(function replaceUrlSlashes( + req: express.Request, + res: express.Response, + next: express.NextFunction, + ) { + req.url = req.url.replace(/\/{2,}/g, '/'); + next(); + }); + + // Set timeout + app.use(timeout(config.timeout) as any); + + // Add body parser + app.use(bodyParser.json({ limit: '20mb' })); +} + +/** + * Create error handling middleware + */ +export function createErrorHandler(debugLogger: debug.Debugger) { + return function ( + err: any, + req: express.Request, + res: express.Response, + _next: express.NextFunction, + ) { + debugLogger('Error: ' + (err && err.message ? err.message : String(err))); + const statusCode = err && err.status ? err.status : 500; + const result = { + error: err && err.message ? err.message : String(err), + name: err && err.name ? err.name : 'Error', + code: err && err.code ? err.code : undefined, + version: pjson.version, + }; + return res.status(statusCode).json(result); + }; +} + +/** + * Create HTTP server + */ +export function createHttpServer(app: express.Application): http.Server { + return http.createServer(app); +} + +/** + * Configure server timeouts + */ +export function configureServerTimeouts(server: https.Server | http.Server, config: Config): void { + if (config.keepAliveTimeout !== undefined) { + server.keepAliveTimeout = config.keepAliveTimeout; + } + if (config.headersTimeout !== undefined) { + server.headersTimeout = config.headersTimeout; + } +} + +/** + * Prepare IPC socket + */ +export async function prepareIpc(ipcSocketFilePath: string): Promise { + if (process.platform === 'win32') { + throw new Error(`IPC option is not supported on platform ${process.platform}`); + } + try { + const stat = fs.statSync(ipcSocketFilePath); + if (!stat.isSocket()) { + throw new Error('IPC socket is not actually a socket'); + } + fs.unlinkSync(ipcSocketFilePath); + } catch (e: any) { + if (e.code !== 'ENOENT') { + throw e; + } + } +} + +/** + * Read SSL/TLS certificates from files + */ +export async function readCertificates( + keyPath: string, + crtPath: string, +): Promise<{ key: string; cert: string }> { + const privateKeyPromise = fs.promises.readFile(keyPath, 'utf8'); + const certificatePromise = fs.promises.readFile(crtPath, 'utf8'); + const [key, cert] = await Promise.all([privateKeyPromise, certificatePromise]); + return { key, cert }; +} + +/** + * Setup common health check routes + */ +export function setupHealthCheckRoutes(app: express.Application, serverType: string): void { + app.get('/ping', (_req, res) => { + res.json({ + status: `${serverType} server is ok!`, + timestamp: new Date().toISOString(), + }); + }); + + app.get('/version', (_req, res) => { + res.json({ + version: pjson.version, + name: pjson.name, + }); + }); +} diff --git a/src/types.ts b/src/types.ts index 2da432c6..933051c1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,11 +7,29 @@ export enum TlsMode { MTLS = 'mtls', // TLS with both server and client certs } -export interface Config { +export enum AppMode { + ENCLAVED = 'enclaved', + MASTER_EXPRESS = 'master-express', +} + +export type EnvironmentName = 'prod' | 'test' | 'staging' | 'dev' | 'local'; + +// Common base configuration shared by both modes +interface BaseConfig { + appMode: AppMode; port: number; bind: string; ipc?: string; debugNamespace?: string[]; + logFile?: string; + timeout: number; + keepAliveTimeout?: number; + headersTimeout?: number; +} + +// Enclaved mode specific configuration +export interface EnclavedConfig extends BaseConfig { + appMode: AppMode.ENCLAVED; // TLS settings keyPath?: string; crtPath?: string; @@ -22,9 +40,27 @@ export interface Config { mtlsRequestCert?: boolean; mtlsRejectUnauthorized?: boolean; mtlsAllowedClientFingerprints?: string[]; - // Other settings - logFile?: string; - timeout: number; - keepAliveTimeout?: number; - headersTimeout?: number; } + +// Master Express mode specific configuration +export interface MasterExpressConfig extends BaseConfig { + appMode: AppMode.MASTER_EXPRESS; + // BitGo API settings + env: EnvironmentName; + customRootUri?: string; + enableSSL?: boolean; + enableProxy?: boolean; + disableEnvCheck?: boolean; + authVersion?: number; + enclavedExpressUrl: string; + enclavedExpressSSLCert: string; + customBitcoinNetwork?: string; + // SSL settings (different from enclaved TLS) + keyPath?: string; + crtPath?: string; + sslKey?: string; + sslCert?: string; +} + +// Union type for the configuration +export type Config = EnclavedConfig | MasterExpressConfig; diff --git a/yarn.lock b/yarn.lock index 85a77826..4456bde5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -564,6 +564,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@noble/hashes@^1.1.5": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -585,6 +590,13 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@paralleldrive/cuid2@^2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz#7f91364d53b89e2c9cb9e02e8dd0f129e834455f" + integrity sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA== + dependencies: + "@noble/hashes" "^1.1.5" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -1097,6 +1109,11 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + async@^3.2.3: version "3.2.6" resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" @@ -1398,7 +1415,7 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== -component-emitter@^1.2.0: +component-emitter@^1.2.0, component-emitter@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== @@ -1450,7 +1467,7 @@ cookie@0.4.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== -cookiejar@^2.1.0: +cookiejar@^2.1.0, cookiejar@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== @@ -1560,6 +1577,14 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -1912,6 +1937,11 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + fastq@^1.6.0: version "1.19.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" @@ -2033,6 +2063,16 @@ formidable@^1.2.0: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.1.2: + version "2.1.5" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.5.tgz#dd7ef4d55c164afaf9b6eb472bfd04b02d66d2dd" + integrity sha512-Oz5Hwvwak/DCaXVVUtPn4oLMLLy1CdclLKO1LFgU7XzDpVMUU5UjlSLpGMocyQNNk8F6IJW9M/YdooSn2MRI+Q== + dependencies: + "@paralleldrive/cuid2" "^2.2.2" + dezalgo "^1.0.4" + once "^1.4.0" + qs "^6.11.0" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -3043,6 +3083,11 @@ mime@1.6.0, mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -3204,7 +3249,7 @@ on-headers@~1.0.1, on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== -once@^1.3.0: +once@^1.3.0, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== @@ -3440,7 +3485,7 @@ qs@6.9.7: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== -qs@^6.5.1: +qs@^6.11.0, qs@^6.5.1: version "6.14.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930" integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== @@ -3582,7 +3627,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.7.2: +semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.7.2: version "7.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== @@ -3875,6 +3920,22 @@ superagent@^3.8.3: qs "^6.5.1" readable-stream "^2.3.5" +superagent@^8.0.9: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + supertest@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/supertest/-/supertest-4.0.2.tgz#c2234dbdd6dc79b6f15b99c8d6577b90e4ce3f36"