import { pathToFileURL } from 'node:url'; import { createApp } from './app.js'; import { type AppConfig, loadConfig } from './config.js'; import type { AppContext } from './context.js'; import { createDatabase } from './db.js'; import { createLogger } from './logging.js'; import { AuthIdentityRepository } from './repositories/authIdentityRepository.js'; import { AuthAuditLogRepository } from './repositories/authAuditLogRepository.js'; import { AuthRiskBlockRepository } from './repositories/authRiskBlockRepository.js'; import { RuntimeRepository } from './repositories/runtimeRepository.js'; import { SmsAuthEventRepository } from './repositories/smsAuthEventRepository.js'; import { UserRepository } from './repositories/userRepository.js'; import { UserSessionRepository } from './repositories/userSessionRepository.js'; import { CustomWorldSessionStore } from './services/customWorldSessionStore.js'; import { UpstreamLlmClient } from './services/llmClient.js'; import { CaptchaChallengeStore } from './services/captchaChallengeStore.js'; import { createSmsVerificationService } from './services/smsVerificationService.js'; import { createWechatAuthService } from './services/wechatAuthService.js'; import { WechatAuthStateStore } from './services/wechatAuthStateStore.js'; function resolveListenTarget(serverAddr: string) { const trimmed = serverAddr.trim(); if (!trimmed) { return { host: '0.0.0.0', port: 8081 }; } if (trimmed.startsWith(':')) { return { host: '0.0.0.0', port: Number(trimmed.slice(1)), }; } if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { const url = new URL(trimmed); return { host: url.hostname, port: Number(url.port || 80), }; } if (trimmed.includes(':')) { const [host, portText] = trimmed.split(':'); return { host: host || '0.0.0.0', port: Number(portText), }; } return { host: '0.0.0.0', port: Number(trimmed), }; } function describeDatabase(databaseUrl: string) { if (databaseUrl.startsWith('pg-mem://')) { return { database_engine: 'pg-mem', database_name: databaseUrl.slice('pg-mem://'.length) || 'memory', }; } try { const url = new URL(databaseUrl); return { database_engine: url.protocol.replace(/:$/u, ''), database_host: url.hostname, database_port: Number(url.port || 5432), database_name: url.pathname.replace(/^\/+/u, '') || 'postgres', }; } catch { return { database_engine: 'postgresql', database_target: 'configured', }; } } export async function createAppContext(config: AppConfig = loadConfig()) { const logger = createLogger(config); const db = await createDatabase(config); const context: AppContext = { config, logger, db, userRepository: new UserRepository(db), authIdentityRepository: new AuthIdentityRepository(db), authAuditLogRepository: new AuthAuditLogRepository(db), authRiskBlockRepository: new AuthRiskBlockRepository(db), smsAuthEventRepository: new SmsAuthEventRepository(db), userSessionRepository: new UserSessionRepository(db), runtimeRepository: new RuntimeRepository(db), llmClient: new UpstreamLlmClient(config, logger), customWorldSessions: new CustomWorldSessionStore(), smsVerificationService: createSmsVerificationService(config, logger), wechatAuthService: createWechatAuthService(config, logger), wechatAuthStates: new WechatAuthStateStore(), captchaChallenges: new CaptchaChallengeStore(), }; return context; } async function main() { const context = await createAppContext(); const app = createApp(context); const { host, port } = resolveListenTarget(context.config.serverAddr); const server = app.listen(port, host, () => { context.logger.info( { host, port, ...describeDatabase(context.config.databaseUrl), }, 'server-node started', ); }); let shuttingDown = false; const shutdown = () => { if (shuttingDown) { return; } shuttingDown = true; context.logger.info('server-node shutting down'); server.close(() => { void context.db .close() .then(() => { process.exit(0); }) .catch((error) => { context.logger.error({ err: error }, 'failed to close database'); process.exit(1); }); }); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } const isEntryPoint = typeof process.argv[1] === 'string' && import.meta.url === pathToFileURL(process.argv[1]).href; if (isEntryPoint) { void main().catch((error) => { console.error(error); process.exit(1); }); }