UNPKG

@interopio/gateway-server

Version:

[![npm version](https://img.shields.io/npm/v/@interopio/gateway-server.svg)](https://www.npmjs.com/package/@interopio/gateway-server)

898 lines (802 loc) 31.9 kB
#!/usr/bin/env node 'use strict'; const { parseArgs } = require('node:util'); const { configure } = require('@interopio/gateway/logging/core'); // import { GatewayServer } from './src/index.ts'; // import { mkcert, argon2, manage } from './src/tools/index.ts'; const { mkcert, argon2, manage, build } = require('@interopio/gateway-server/tools'); const { GatewayServer } = require('@interopio/gateway-server'); // --------------------------------------------------------------------------- // Declarative command registry — drives both help text and parseArgs calls // --------------------------------------------------------------------------- const commands = { run: { description: 'Start the gateway server', options: { port: { type: 'string', short: 'p', help: 'Port or port range (default: 0 i.e. random)', example: '8385, 8000-8100, 3000,4000-4050' }, host: { type: 'string', short: 'H', help: 'Network address to bind to', example: 'localhost, 127.0.0.1, 0.0.0.0, ::1' }, user: { type: 'string', short: 'u', help: 'Enable basic auth and set admin username' }, auth: { type: 'boolean', help: 'Enable/disable authentication (--no-auth overrides config)' }, ssl: { type: 'boolean', short: 'S', help: 'Enable HTTPS (auto-generates certs if needed)' }, tls: { type: 'boolean', help: 'Alias for --ssl' }, cert: { type: 'string', help: 'SSL/TLS certificate file (default: ./gateway-server.crt)' }, key: { type: 'string', help: 'SSL/TLS private key file (default: ./gateway-server.key)' }, ca: { type: 'string', help: 'CA certificate file (default: ./gateway-ca.crt)' }, 'ca-key': { type: 'string', help: 'CA private key file (default: ./gateway-ca.key)' }, gateway: { type: 'boolean', short: 'g', help: 'Enable/disable gateway endpoint (--no-gateway)' }, static: { type: 'string', short: 's', multiple: true, help: 'Serve static files from location' }, config: { type: 'string', short: 'c', help: 'Server configuration file (JSON)' }, debug: { type: 'boolean', help: 'Enable debug logging (default: info)' }, }, allowNegative: true, examples: [ 'gateway-server run -p 3000', 'gateway-server run -u admin --port 8385,8388 --debug', 'gateway-server run -p 8443 --ssl --user admin --gateway', 'gateway-server run --port 42443 --tls --ca-key ./ssl/ca.key --host example.com --gateway', 'gateway-server run -p 3000 --static ./public --config ./server-config.json', 'gateway-server run --config ./server-config.json --no-gateway --no-ssl --no-auth', ], }, build: { description: 'Build a Single Executable Application (SEA)', positionals: '[entryPoints...]', options: { 'app-name': { type: 'string', help: 'Base name for the executable (default: package.json name)' }, 'app-version': { type: 'string', help: 'App version in output dir name (default: package.json version)' }, executable: { type: 'string', help: 'Path to the Node.js binary (default: current node)' }, outdir: { type: 'string', short: 'o', help: 'Output directory for build artifacts (default: ./build)' }, }, examples: [ 'gateway-server build', 'gateway-server build --app-version 1.2.3', 'gateway-server build ./src/main.cjs --app-name io-bridge --executable /usr/local/bin/node22 --app-version 2.0.0', ], }, manage: { description: 'Send management commands to a running gateway server', positionals: '<command> [key=value... or json]', options: { path: { type: 'string', help: 'Gateway control socket (named pipe / Unix socket)', example: '\\\\\\\\.\\\\pipe\\\\glue42-gateway-xxx, /tmp/gateway.sock' }, port: { type: 'string', short: 'p', help: 'TCP port of the management server' }, timeout: { type: 'string', help: 'Connection timeout in ms (default: 5000)' }, }, examples: [ 'gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info', 'gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx shutdown', 'gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx custom-cmd key1=value1 count=42', 'gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx \'{"command":"update-auth","type":"basic"}\'', ], }, passwd: { description: 'Generate password hash (default: Argon2)', options: { stdin: { type: 'boolean', help: 'Read password from stdin (for piping)' }, }, examples: [ 'gateway-server passwd', 'echo "mySecret123" | gateway-server passwd --stdin', ], }, mkcert: { description: 'Generate client/server certificate signed by Dev CA', positionals: '[name...]', options: { client: { type: 'boolean', help: 'Generate client certificate (default: server)' }, user: { type: 'string', short: 'u', help: 'Common Name (default: dev-user, --client only)' }, ca: { type: 'string', help: 'CA certificate file (default: ./gateway-ca.crt)' }, 'ca-key': { type: 'string', help: 'CA private key file (default: ./gateway-ca.key)' }, key: { type: 'string', help: 'Output private key file' }, cert: { type: 'string', help: 'Output certificate file' }, }, examples: [ 'gateway-server mkcert', 'gateway-server mkcert localhost 127.0.0.1 IP:192.168.1.100', 'gateway-server mkcert --client EMAIL:john.doe@example.com', 'gateway-server mkcert example.com *.example.com --key ./gateway-server.key --cert ./gateway-server.crt', ], }, }; // --------------------------------------------------------------------------- // Help text generator // --------------------------------------------------------------------------- function formatOption(name, opt, negatable, col) { const shortFlag = opt.short ? `-${opt.short}, ` : ' '; const flag = (opt.type === 'boolean' && negatable) ? `--[no-]${name}` : `--${name}`; const argHint = opt.type === 'string' ? ` <${name.replace(/-/g, '_')}>` : ''; const left = ` ${shortFlag}${flag}${argHint}`; const pad = Math.max(2, col - left.length); let line = left + ' '.repeat(pad) + (opt.help || ''); if (opt.example) { line += `\n${' '.repeat(col)}examples: ${opt.example}`; } return line; } /** Compute column width from a set of options. */ function optionColumnWidth(options, negatable) { let max = 0; for (const [name, opt] of Object.entries(options)) { const shortFlag = opt.short ? `-X, ` : ' '; const flag = (opt.type === 'boolean' && negatable) ? `--[no-]${name}` : `--${name}`; const argHint = opt.type === 'string' ? ` <${name.replace(/-/g, '_')}>` : ''; max = Math.max(max, ` ${shortFlag}${flag}${argHint}`.length); } return max + 2; // 2 chars minimum padding } function showHelp() { const lines = [ '', 'Usage: gateway-server <command> [options]', '', 'Commands:', ]; // Command list for (const [name, cmd] of Object.entries(commands)) { lines.push(` ${name.padEnd(24)} ${cmd.description}`); } // Per-command options for (const [name, cmd] of Object.entries(commands)) { lines.push(''); const positionals = cmd.positionals ? ` ${cmd.positionals}` : ''; lines.push(`${name}${positionals} options:`); const col = optionColumnWidth(cmd.options, cmd.allowNegative); for (const [optName, opt] of Object.entries(cmd.options)) { lines.push(formatOption(optName, opt, cmd.allowNegative, col)); } } // Global options lines.push(''); lines.push('Global options:'); lines.push(' -v, --version Show version information and exit'); lines.push(' -h, --help Show this help message and exit'); // Examples lines.push(''); lines.push('Examples:'); for (const cmd of Object.values(commands)) { for (const ex of (cmd.examples || [])) { lines.push(` ${ex}`); } } console.log(lines.join('\n')); process.exit(0); } /** Extract parseArgs-compatible options from a command definition (strips help/example). */ function argsOptions(name) { const cmd = commands[name]; const opts = { help: { type: 'boolean', short: 'h' } }; for (const [k, v] of Object.entries(cmd.options)) { const o = { type: v.type }; if (v.short) o.short = v.short; if (v.multiple) o.multiple = true; opts[k] = o; } return opts; } /** Show help for a single command and exit. */ function showCommandHelp(name) { const cmd = commands[name]; const positionals = cmd.positionals ? ` ${cmd.positionals}` : ''; const lines = [ '', `Usage: gateway-server ${name}${positionals} [options]`, '', cmd.description, '', 'Options:', ]; const col = optionColumnWidth(cmd.options, cmd.allowNegative); for (const [optName, opt] of Object.entries(cmd.options)) { lines.push(formatOption(optName, opt, cmd.allowNegative, col)); } const helpLeft = ' -h, --help'; lines.push(helpLeft + ' '.repeat(Math.max(2, col - helpLeft.length)) + 'Show this help message'); if (cmd.examples && cmd.examples.length > 0) { lines.push(''); lines.push('Examples:'); for (const ex of cmd.examples) { lines.push(` ${ex}`); } } console.log(lines.join('\n')); process.exit(0); } function showVersion() { console.log(GatewayServer.VERSION); process.exit(0); } /** Read the nearest package.json starting from cwd, walking up. */ function readPackageJson() { const { readFileSync } = require('node:fs'); const { join, dirname } = require('node:path'); let dir = process.cwd(); while (true) { try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { const parent = dirname(dir); if (parent === dir) return {}; dir = parent; } } } // Parse command-line arguments const { values: globalValues, positionals } = parseArgs({ args: process.argv.slice(2), options: { version: { type: 'boolean', short: 'v' }, help: { type: 'boolean', short: 'h' }, }, strict: false, allowPositionals: true, }); // Check for global flags first if (globalValues.version) { showVersion(); } if (globalValues.help && positionals.length === 0) { showHelp(); } // Determine command (required) if (positionals.length === 0) { console.error('Error: Command is required'); console.log('Use --help to see available commands'); process.exit(1); } const command = positionals[0]; // Route to appropriate command handler switch (command) { case 'run': runServer().catch(e => {console.error(e); process.exit(1);}); break; case 'build': buildCommand().catch(e => {console.error(e); process.exit(1);}); break; case 'manage': manageCommand().catch(e => {console.error(e); process.exit(1);}); break; case 'passwd': passwdCommand().catch(e => {console.error(e); process.exit(1);}); break; case 'mkcert': mkcertCommand().catch(e => {console.error(e); process.exit(1);}); break; default: console.error(`Error: Unknown command "${command}"`); console.log('Use --help to see available commands'); process.exit(1); } // Command: build - Build a Single Executable Application (SEA) async function buildCommand() { const { values, positionals } = parseArgs({ args: process.argv.slice(3), options: argsOptions('build'), strict: true, allowPositionals: true, }); if (values.help) showCommandHelp('build'); // Default app-name and app-version from the nearest package.json const pkg = readPackageJson(); const name = values['app-name'] || (pkg.name ? pkg.name.replace(/^@.*\//, '') : 'gateway-server'); const entryPoints = positionals.length > 0 ? positionals : [`./bin/${name}.cjs`]; const executable = values.executable; if (!executable) { try { if (require('node:sea').isSea()) { console.error('Error: --executable is required when running from a single executable application.'); console.error('Provide the path to a Node.js binary, e.g. --executable /usr/local/bin/node'); process.exit(1); } } catch { // not a SEA build } } const targetPath = await build.buildSea(name, { version: values['app-version'] || pkg.version, outDir: values.outdir, esbuild: { entryPoints, define: { 'process.env.NODE_ENV': '"production"', 'process.env.WS_NO_UTF_8_VALIDATE': 'true', 'process.env.WS_NO_BUFFER_UTIL': 'true' }, }, config: { executable, execArgv: ['--experimental-websocket'], execArgvExtension: 'cli', }, }); console.log(`SEA executable built: ${targetPath}`); process.exit(0); } // Command: manage - Send management commands to a running gateway server async function manageCommand() { const { values, positionals } = parseArgs({ args: process.argv.slice(3), options: argsOptions('manage'), strict: true, allowPositionals: true, }); if (values.help) showCommandHelp('manage'); const socketPath = values.path; const port = values.port ? parseInt(values.port, 10) : undefined; const timeout = values.timeout ? parseInt(values.timeout, 10) : undefined; const firstArg = positionals[0]; if (!socketPath && !port) { console.error('Error: --path or --port is required'); console.error('Example: gateway-server manage --path \\\\.\\pipe\\glue42-gateway-xxx info'); process.exit(1); } if (!firstArg) { console.error('Error: management command is required'); console.error('Available commands: info, shutdown'); console.error('Or pass a JSON object: gateway-server manage --path ... \'{"command":"info"}\''); process.exit(1); } // Try to parse first argument as JSON (entire command object) let commandParams; if (firstArg.startsWith('{')) { try { commandParams = JSON.parse(firstArg); if (!commandParams.command) { console.error('Error: JSON command must have a "command" property'); process.exit(1); } } catch (e) { console.error(`Error: invalid JSON: ${e.message}`); process.exit(1); } } else { // Parse as command name + key=value pairs commandParams = { command: firstArg }; for (let i = 1; i < positionals.length; i++) { const arg = positionals[i]; const eqIndex = arg.indexOf('='); if (eqIndex === -1) { console.error(`Error: invalid parameter "${arg}". Expected format: key=value`); process.exit(1); } const key = arg.slice(0, eqIndex); let value = arg.slice(eqIndex + 1); // Try to parse as JSON for numbers, booleans, arrays, objects try { value = JSON.parse(value); } catch { // Keep as string if not valid JSON } commandParams[key] = value; } } const server = socketPath ? { path: socketPath } : { port }; try { const result = await manage.sendCommand(server, commandParams, { timeout }); if (result === undefined) { console.log('<no response>'); } else if (typeof result === 'string') { console.log(result); } else { console.log(JSON.stringify(result, null, 2)); } process.exit(0); } catch (e) { console.error(`Error: ${e.message || e}`); process.exit(1); } } // Command: passwd - Generate password hash async function passwdCommand() { const { values } = parseArgs({ args: process.argv.slice(3), options: argsOptions('passwd'), strict: true, allowPositionals: false, }); if (values.help) showCommandHelp('passwd'); let password/*: string = undefined!*/; // Read password from stdin if specified if (values.stdin) { password = await readPasswordFromStdin(); } // If no password provided, prompt interactively if (!password) { password = await promptPassword('Enter password: '); } if (!password) { console.error('Error: password is required'); process.exit(1); } const hashValue = await argon2.hash(password); const hash = `{argon2id}${hashValue}`; console.log(hash); process.exit(0); } // Read password from piped stdin (non-interactive) async function readPasswordFromStdin() { // Check if stdin is a TTY (interactive terminal) if (process.stdin.isTTY) { // Interactive mode - use readline with hidden input return promptPassword('Password: '); } // Piped mode - read until EOF or newline const chunks/*: Uint8Array[]*/ = []; for await (const chunk of process.stdin) { chunks.push(chunk); } const content = Buffer.concat(chunks).toString('utf8'); // Strip trailing newline return content.replace(/\r?\n$/, ''); } // Prompt for password with masked input async function promptPassword(prompt/*: string*/) { return new Promise/*<string>*/((resolve) => { process.stdout.write(prompt); // Only set raw mode if stdin is a TTY if (!process.stdin.isTTY) { console.error('Error: Interactive password prompt requires a terminal'); process.exit(1); } process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8'); let password = ''; const cleanup = () => { process.stdin.setRawMode(false); process.stdin.pause(); process.stdin.removeListener('data', onData); process.stdout.write('\n'); }; const onData = (char/*: string*/) => { // Ctrl+C if (char === '\u0003') { cleanup(); process.exit(1); } // Enter if (char === '\r' || char === '\n') { cleanup(); resolve(password); return; } // Backspace if (char === '\u007f' || char === '\b') { if (password.length > 0) { password = password.slice(0, -1); process.stdout.write('\b \b'); } return; } // Regular character password += char; process.stdout.write('*'); }; process.stdin.on('data', onData); }); } // Command: mkcert - Generate client or server certificate async function mkcertCommand() { const { values, positionals } = parseArgs({ args: process.argv.slice(3), options: argsOptions('mkcert'), strict: true, allowPositionals: true, }); if (values.help) showCommandHelp('mkcert'); const isClientCert = values.client || false; const user = values.user || 'dev-user'; const caCert = values.ca || './gateway-ca.crt'; const caKey = values['ca-key'] || './gateway-ca.key'; let keyPath = values.key; let certPath = values.cert; const sanEntries = positionals; // Default output paths // Users typically specify just --cert, and key should go in the same file if (!keyPath && !certPath) { // Neither specified: use defaults based on certificate type if (isClientCert) { // Client certificate: combined file by default keyPath = './gateway-client.key'; certPath = './gateway-client.crt'; } else { // Server certificate: separate files by default keyPath = './gateway-server.key'; certPath = './gateway-server.crt'; } } else if (keyPath && !certPath) { // Only --key specified: cert goes in the same file (combined) certPath = keyPath; } else if (!keyPath && certPath) { // Only --cert specified: key goes in the same file (combined) keyPath = certPath; } // else: both specified, use as-is if (sanEntries.length === 0) { if (isClientCert) { sanEntries.push(user); } else { // Default SAN entry for server certs sanEntries.push('localhost'); } } const { readFile, writeFile } = await import('node:fs/promises'); const { KEYUTIL, X509 } = await import('jsrsasign'); // Load CA key let caKeyObj/*: ReturnType<typeof KEYUTIL.getKey>*/; try { const caKeyPem = await readFile(caKey, 'utf8'); caKeyObj = KEYUTIL.getKey(caKeyPem); } catch (error) { console.error(`Error: Cannot read CA key from ${caKey}`); console.error(error?.['message']); process.exit(1); } // Extract issuer from CA certificate let issuer/*: string*/; try { const caCertPem = await readFile(caCert, 'utf8'); const caCertObj = new X509(); caCertObj.readCertPEM(caCertPem); issuer = caCertObj.getSubjectString(); } catch (error) { console.error(`Error: Cannot read CA certificate from ${caCert}`); console.error(error?.['message']); process.exit(1); } // Generate certificate using SAN entries const cert = mkcert.generateCert(caKeyObj, issuer, sanEntries, isClientCert); // Write files try { if (keyPath === certPath) { // Concatenate cert and key into single file (cert first, then key) const combined = cert.cert + cert.key; await writeFile(keyPath, combined, { mode: 0o600 }); console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`); console.log(` Combined (cert+key): ${keyPath}`); if (sanEntries.length > 0) { console.log(` SAN: ${sanEntries.join(', ')}`); } } else { // Write separate files await writeFile(keyPath, cert.key, { mode: 0o600 }); await writeFile(certPath, cert.cert, { mode: 0o644 }); console.log(`${isClientCert ? 'Client' : 'Server'} certificate generated successfully:`); console.log(` Private key: ${keyPath}`); console.log(` Certificate: ${certPath}`); if (sanEntries.length > 0) { console.log(` SAN: ${sanEntries.join(', ')}`); } } process.exit(0); } catch (error) { console.error(`Error: Cannot write certificate files`); console.error(error?.['message']); process.exit(1); } } // Command: run - Start the server async function runServer() { const { values } = parseArgs({ args: process.argv.slice(3), options: argsOptions('run'), strict: true, allowNegative: true, allowPositionals: false, }); if (values.help) showCommandHelp('run'); const options = { port: values.port, host: values.host, user: values.user, debug: values.debug || false, ssl: values.ssl || values.tls || false, noSsl: values.ssl === false, // --no-ssl explicitly set cert: values.cert, key: values.key, ca: values.ca, caKey: values['ca-key'], config: values.config, gateway: values.gateway, noGateway: values.gateway === false, // --no-gateway explicitly set static: values.static, }; configure({ level: options.debug ? { gateway: 'debug', "gateway.node": 'info', } : 'info', }); // Load server configuration from file if specified let serverConfig/*: GatewayServer.ServerConfig*/ = {}; const { access, constants } = await import('node:fs/promises'); const { resolve, extname } = await import('node:path'); let configPath/*: string*/; for (const fileName of [ 'gateway-server.config.js', 'gateway-server.config.mjs', 'gateway-server.config.cjs', 'gateway-server.config.ts', 'gateway-server.config.mts', 'gateway-server.config.cts', 'gateway-server.config.json' ]) { configPath = resolve(process.cwd(), fileName); try { await access(configPath, constants.R_OK); // found readable config file, load it break; } catch { configPath = undefined; // continue to next candidate } } if (options.config) { configPath = options.config; } if (configPath) { try { if (['.json'].includes(extname(configPath))) { // JSON config file - read and parse manually const { readFile } = await import('node:fs/promises'); const configContent = await readFile(configPath, 'utf8'); serverConfig = JSON.parse(configContent); } else { // JS/TS config file - import dynamically const { pathToFileURL } = await import('node:url'); const importedConfig = await import(pathToFileURL(configPath)); serverConfig = importedConfig.default || importedConfig; } } catch (error) { console.error(`Error: Cannot read server config from ${configPath}`); console.error(error?.['message']); process.exit(1); } } // Apply CLI overrides to server config // CLI arguments take precedence over config file // Override port if specified if (values.port !== undefined) { serverConfig.port = options.port; } // Override host if specified if (values.host !== undefined) { serverConfig.host = options.host; } // Override SSL configuration if (options.noSsl) { // --no-ssl: delete SSL config completely delete serverConfig.ssl; options.ssl = false; } else if (options.ssl) { // --ssl specified: merge CLI args with existing config if (!serverConfig.ssl) { serverConfig.ssl = {}; } // Only set properties if CLI options are explicitly provided if (options.key !== undefined) { serverConfig.ssl.key = options.key; } if (options.cert !== undefined) { serverConfig.ssl.cert = options.cert; } if (options.ca !== undefined) { serverConfig.ssl.ca = options.ca; } } // else: use whatever is in serverConfig (if any) // Determine effective SSL state const effectiveSsl = options.ssl || (!options.noSsl && serverConfig?.ssl !== undefined); // Configure authentication let auth/*: GatewayServer.AuthConfig*/; if (values.auth === false) { // --no-auth: delete auth config from server config and set to 'none' delete serverConfig.auth; auth = { type: 'none' }; } else if (serverConfig.auth) { // Use auth from config file if present auth = serverConfig.auth; // Allow --user CLI option to override auth.user.name from config if (options.user) { if (!auth.user) { auth.user = {name: options.user}; } auth.user.name = options.user; } } else { // No auth in config file, determine from CLI args or defaults auth = { type: options.user ? 'basic' : effectiveSsl ? 'x509' : 'none' }; if (options.user) { auth.user = { name: options.user }; } if (effectiveSsl && auth.type === 'x509') { auth.x509 = { key: options.caKey || './gateway-ca.key', // principalAltName: 'email', }; // Ensure SSL config exists and has proper mutual TLS settings if (!serverConfig.ssl) { serverConfig.ssl = {}; } // Enable client certificate verification with proper mutual TLS serverConfig.ssl.requestCert = true; // Enforce trust verification to prevent certificate spoofing // rejectUnauthorized must be true to ensure only trusted client certs are accepted serverConfig.ssl.rejectUnauthorized = true; // Set CA for client certificate verification (required for mutual TLS) // Use the same CA that signs client certificates if (!serverConfig.ssl.ca) { serverConfig.ssl.ca = options.ca || './gateway-ca.crt'; } } } // Handle SSL certificate auto-generation when --ssl is specified via CLI // but config file doesn't have server certificates if (options.ssl && effectiveSsl && serverConfig.ssl) { const hasCert = serverConfig.ssl.cert || serverConfig.ssl.key; if (!hasCert) { // SSL enabled but no server cert/key provided // We need a CA key for auto-generating server certificates // Set default CA key path if not already configured if (!auth.x509) { auth.x509 = {}; } if (!auth.x509.key) { // Use CLI option, or default to ./gateway-ca.key auth.x509.key = options.caKey || './gateway-ca.key'; } } } // Override gateway configuration if (options.noGateway) { // --no-gateway: delete gateway config completely delete serverConfig.gateway; } else if (options.gateway) { // --gateway specified: enable gateway endpoint, respect existing config if (!serverConfig.gateway) { serverConfig.gateway = {}; } // Set defaults only if not already present in config if (!serverConfig.gateway.route) { serverConfig.gateway.route = '/'; } if (!serverConfig.gateway.authorize) { serverConfig.gateway.authorize = { access: auth.type !== 'none' ? 'authenticated' : 'permitted' }; } } // else: use whatever is in serverConfig (if any) // Override static resources if specified if (options.static && options.static.length > 0) { if (!serverConfig.resources) { serverConfig.resources = {locations: options.static}; } serverConfig.resources.locations = options.static; } // Set auth in server config serverConfig.auth = auth; const server = await GatewayServer.Factory({ ...(serverConfig) }); process.on('SIGINT', async () => { await server.close(); process.exit(0); }); // process.title = `${GatewayServer.VERSION} ${server.address.port}`; }