#!/usr/bin/env node import { initializeSentry } from './sentry/initializeSentry'; initializeSentry(); import { buildAgents } from './agentStore'; import createApp from './index'; import { logger } from './logger'; import { captureException } from './sentry/captureException'; import { Environment } from './utils/env'; import bytes from 'bytes'; import { readFileSync } from 'node:fs'; import tls from 'node:tls'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; const TIMEOUT = 15 * 60 * 1000; // 15 minutes const CLOSE_DELAY = 5_000; const FORCE_EXIT_TIMEOUT = 295_000; const argv = yargs(hideBin(process.argv)) .option('port', { type: 'number', default: Number(Environment({}).PORT), describe: 'Port to listen on', }) .option('body-size-limit', { type: 'string', default: '10mb', describe: 'Maximum request body size (e.g. 10mb, 1gb)', }) .parseSync(); const port = argv.port; const bodySizeLimit = bytes.parse(argv['body-size-limit']); if (!bodySizeLimit) { logger.error({ value: argv['body-size-limit'] }, 'invalid --body-size-limit value'); process.exit(1); } const tlsKeyPath = Environment({}).TLS_KEY_PATH; const tlsCertPath = Environment({}).TLS_CERT_PATH; const tlsCaPath = Environment({}).TLS_CA_PATH; let tlsKey = Environment({}).TLS_KEY; let tlsCert = Environment({}).TLS_CERT; let tlsCa = Environment({}).TLS_CA; const defaultCAs = tls.rootCertificates; if (tlsKeyPath && tlsCertPath) { try { tlsKey = readFileSync(tlsKeyPath, 'utf-8'); tlsCert = readFileSync(tlsCertPath, 'utf-8'); if (tlsCaPath) { tlsCa = readFileSync(tlsCaPath, 'utf-8'); } } catch (error) { logger.error({ err: error }, 'error reading TLS keys'); } } const agentConfig: any = {}; if ((tlsKey && tlsCert) || tlsCa) { agentConfig.tls = { ...(tlsKey && { key: tlsKey }), ...(tlsCert && { cert: tlsCert }), ...(tlsCa ? { ca: [...defaultCAs, tlsCa] } : {}), }; } buildAgents(agentConfig); const httpsOpts = tlsKey && tlsCert ? { https: { key: tlsKey, cert: tlsCert, ...(tlsCa ? { ca: [...defaultCAs, tlsCa] } : {}), }, } : {}; let status: 'running' | 'terminating' = 'running'; let shutdownRequested = false; const app = createApp({ ...httpsOpts, bodyLimit: bodySizeLimit } as any, { getStatus: () => status, }); const requestShutdown = (signal: NodeJS.Signals) => { if (shutdownRequested) { logger.warn({ signal }, 'received second shutdown signal; forcing exit'); // eslint-disable-next-line n/no-process-exit process.exit(1); } shutdownRequested = true; status = 'terminating'; logger.warn( { closeDelayMs: CLOSE_DELAY, forceExitTimeoutMs: FORCE_EXIT_TIMEOUT, signal, }, 'shutdown signal received; draining AI gateway', ); if (typeof app.server.closeIdleConnections === 'function') { app.server.closeIdleConnections(); } setTimeout(() => { void app .close() .then(() => { logger.info('AI Gateway stopped'); // eslint-disable-next-line n/no-process-exit process.exit(0); }) .catch((error) => { logger.error({ err: error }, 'failed to stop AI Gateway'); // eslint-disable-next-line n/no-process-exit process.exit(1); }); }, CLOSE_DELAY).unref(); setTimeout(() => { logger.error({ signal }, 'shutdown timeout exceeded; forcing exit'); // eslint-disable-next-line n/no-process-exit process.exit(1); }, FORCE_EXIT_TIMEOUT).unref(); }; for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.once(signal, () => { requestShutdown(signal); }); } app.listen({ port, host: '::' }, (err) => { if (err) { logger.error({ err }, 'failed to start server'); process.exit(1); } const server = app.server; server.setTimeout(TIMEOUT); server.requestTimeout = TIMEOUT; server.headersTimeout = TIMEOUT; logger.info({ port }, 'AI Gateway started'); }); process.on('uncaughtException', (err) => { logger.error({ err }, 'uncaught exception'); captureException({ error: err, message: 'uncaught exception', }); }); process.on('unhandledRejection', (err) => { logger.error({ err }, 'unhandled rejection'); captureException({ error: err, message: 'unhandled rejection', }); });