import type Redis from 'ioredis' import type { Env } from './libs/env' import type { OtelInstance } from './libs/otel' import type { BillingMqService } from './services/billing/billing-mq' import type { BillingService } from './services/billing/billing-service' import type { CharacterService } from './services/characters' import type { ChatService } from './services/chats' import type { ConfigKVService } from './services/config-kv' import type { FluxService } from './services/flux' import type { FluxAuditService } from './services/flux-audit' import type { ProviderService } from './services/providers' import type { StripeService } from './services/stripe' import type { HonoEnv } from './types/hono' import process from 'node:process' import { initLogger, LoggerFormat, LoggerLevel, useLogger } from '@guiiai/logg' import { serve } from '@hono/node-server' import { createNodeWebSocket } from '@hono/node-ws' import { Hono } from 'hono' import { bodyLimit } from 'hono/body-limit' import { cors } from 'hono/cors' import { logger as honoLogger } from 'hono/logger' import { createLoggLogger, injeca, lifecycle } from 'injeca' import { createAuth } from './libs/auth' import { createDrizzle, migrateDatabase } from './libs/db' import { parsedEnv } from './libs/env' import { initializeExternalDependency } from './libs/external-dependency' import { initOtel } from './libs/otel' import { createRedis } from './libs/redis' import { sessionMiddleware } from './middlewares/auth' import { otelMiddleware } from './middlewares/otel' import { rateLimiter } from './middlewares/rate-limit' import { createCharacterRoutes } from './routes/characters' import { createChatWsHandlers } from './routes/chat-ws' import { createChatRoutes } from './routes/chats' import { createFluxRoutes } from './routes/flux' import { createProviderRoutes } from './routes/providers' import { createStripeRoutes } from './routes/stripe' import { createV1CompletionsRoutes } from './routes/v1completions' import { createBillingMqService } from './services/billing/billing-mq' import { createBillingService } from './services/billing/billing-service' import { createCharacterService } from './services/characters' import { createChatService } from './services/chats' import { createConfigKVService } from './services/config-kv' import { createFluxService } from './services/flux' import { createFluxAuditService } from './services/flux-audit' import { createProviderService } from './services/providers' import { createRequestLogService } from './services/request-log' import { createStripeService } from './services/stripe' import { ApiError, createInternalError, createUnauthorizedError } from './utils/error' import { getTrustedOrigin } from './utils/origin' interface AppDeps { auth: ReturnType characterService: CharacterService chatService: ChatService providerService: ProviderService fluxService: FluxService fluxAuditService: FluxAuditService stripeService: StripeService billingService: BillingService billingMqService: BillingMqService configKV: ConfigKVService redis: Redis env: Env otel: OtelInstance | null } function buildApp(deps: AppDeps) { const logger = useLogger('app').useGlobalConfig() const app = new Hono() .use( '/api/*', cors({ origin: origin => getTrustedOrigin(origin), credentials: true, }), ) .use(honoLogger()) if (deps.otel) { app.use('*', otelMiddleware(deps.otel.http)) } // WebSocket setup — must be registered BEFORE bodyLimit middleware const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }) const chatWsSetup = createChatWsHandlers(deps.chatService, deps.redis, deps.otel?.engagement ?? null) app.get('/ws/chat', upgradeWebSocket(async (c) => { const token = c.req.query('token') if (!token) { throw createUnauthorizedError('Missing token') } const session = await deps.auth.api.getSession({ headers: new Headers({ Authorization: `Bearer ${token}` }), }) if (!session?.user) { throw createUnauthorizedError('Invalid token') } return chatWsSetup(session.user.id) })) const builtApp = app .use('*', sessionMiddleware(deps.auth)) .use('*', bodyLimit({ maxSize: 1024 * 1024 })) .onError((err, c) => { if (err instanceof ApiError) { logger.withError(err).warn('API error occurred') return c.json({ error: err.errorCode, message: err.message, details: err.details, }, err.statusCode) } logger.withError(err).error('Unhandled error') const internalError = createInternalError() return c.json({ error: internalError.errorCode, message: internalError.message, }, internalError.statusCode) }) /** * Health check route. */ .on('GET', '/health', c => c.json({ status: 'ok' })) /** * Auth routes are handled by the auth instance directly, * Powered by better-auth. * Rate limited by IP: 20 requests per minute. */ .use('/api/auth/*', rateLimiter({ max: 20, windowSec: 60, keyGenerator: c => c.req.header('x-forwarded-for') ?? c.req.header('x-real-ip') ?? 'unknown', })) .on(['POST', 'GET'], '/api/auth/*', c => deps.auth.handler(c.req.raw)) /** * Character routes are handled by the character service. */ .route('/api/characters', createCharacterRoutes(deps.characterService)) /** * Provider routes are handled by the provider service. */ .route('/api/providers', createProviderRoutes(deps.providerService)) /** * Chat routes are handled by the chat service. */ .route('/api/chats', createChatRoutes(deps.chatService)) /** * V1 routes for official provider. */ .route('/api/v1', createV1CompletionsRoutes(deps.fluxService, deps.billingService, deps.configKV, deps.billingMqService, deps.otel?.llm)) /** * Flux routes. */ .route('/api/flux', createFluxRoutes(deps.fluxService, deps.fluxAuditService)) /** * Stripe routes. */ .route('/api/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.otel?.revenue)) return { app: builtApp, injectWebSocket } } export type AppType = ReturnType['app'] export async function createApp() { initLogger(LoggerLevel.Debug, LoggerFormat.Pretty) injeca.setLogger(createLoggLogger(useLogger('injeca').useGlobalConfig())) const logger = useLogger('app').useGlobalConfig() const otel = injeca.provide('libs:otel', { dependsOn: { env: parsedEnv, lifecycle }, build: ({ dependsOn }) => { const o = initOtel(dependsOn.env) if (!o) return null dependsOn.lifecycle.appHooks.onStop(() => o.shutdown()) return o }, }) const db = injeca.provide('datastore:db', { dependsOn: { env: parsedEnv, lifecycle }, build: async ({ dependsOn }) => { const { db: dbInstance, pool } = await initializeExternalDependency( 'Database', logger, async (attempt) => { const connection = createDrizzle(dependsOn.env.DATABASE_URL) try { await connection.db.execute('SELECT 1') logger.log(`Connected to database on attempt ${attempt}`) await migrateDatabase(connection.db) logger.log(`Applied schema on attempt ${attempt}`) return connection } catch (error) { await connection.pool.end() throw error } }, ) dependsOn.lifecycle.appHooks.onStop(() => pool.end()) return dbInstance }, }) const redis = injeca.provide('datastore:redis', { dependsOn: { env: parsedEnv, lifecycle }, build: async ({ dependsOn }) => { const redisInstance = await initializeExternalDependency( 'Redis', logger, async (attempt) => { const instance = createRedis(dependsOn.env.REDIS_URL) try { await instance.connect() logger.log(`Connected to Redis on attempt ${attempt}`) return instance } catch (error) { instance.disconnect() throw error } }, ) dependsOn.lifecycle.appHooks.onStop(async () => { await redisInstance.quit() }) return redisInstance }, }) const configKV = injeca.provide('datastore:configKV', { dependsOn: { redis }, build: ({ dependsOn }) => createConfigKVService(dependsOn.redis), }) const billingMqService = injeca.provide('services:billingMq', { dependsOn: { redis, env: parsedEnv }, build: ({ dependsOn }) => createBillingMqService(dependsOn.redis, { stream: dependsOn.env.BILLING_EVENTS_STREAM, }), }) const auth = injeca.provide('services:auth', { dependsOn: { db, env: parsedEnv, otel }, build: ({ dependsOn }) => createAuth(dependsOn.db, dependsOn.env, dependsOn.otel?.auth), }) const characterService = injeca.provide('services:characters', { dependsOn: { db, otel }, build: ({ dependsOn }) => createCharacterService(dependsOn.db, dependsOn.otel?.engagement), }) const providerService = injeca.provide('services:providers', { dependsOn: { db }, build: ({ dependsOn }) => createProviderService(dependsOn.db), }) const chatService = injeca.provide('services:chats', { dependsOn: { db, otel }, build: ({ dependsOn }) => createChatService(dependsOn.db, dependsOn.otel?.engagement), }) const stripeService = injeca.provide('services:stripe', { dependsOn: { db }, build: ({ dependsOn }) => createStripeService(dependsOn.db), }) const fluxAuditService = injeca.provide('services:fluxAudit', { dependsOn: { db }, build: ({ dependsOn }) => createFluxAuditService(dependsOn.db), }) const fluxService = injeca.provide('services:flux', { dependsOn: { db, redis, configKV }, build: ({ dependsOn }) => createFluxService(dependsOn.db, dependsOn.redis, dependsOn.configKV), }) const requestLogService = injeca.provide('services:requestLog', { dependsOn: { db }, build: ({ dependsOn }) => createRequestLogService(dependsOn.db), }) const billingService = injeca.provide('services:billing', { dependsOn: { db, redis, billingMqService, configKV, otel }, build: ({ dependsOn }) => createBillingService(dependsOn.db, dependsOn.redis, dependsOn.billingMqService, dependsOn.configKV, dependsOn.otel?.revenue), }) await injeca.start() const resolved = await injeca.resolve({ db, auth, characterService, chatService, providerService, fluxService, fluxAuditService, requestLogService, stripeService, billingService, billingMqService, configKV, redis, env: parsedEnv, otel, }) const { app, injectWebSocket } = buildApp({ auth: resolved.auth, characterService: resolved.characterService, chatService: resolved.chatService, providerService: resolved.providerService, fluxService: resolved.fluxService, fluxAuditService: resolved.fluxAuditService, stripeService: resolved.stripeService, billingService: resolved.billingService, billingMqService: resolved.billingMqService, configKV: resolved.configKV, redis: resolved.redis, env: resolved.env, otel: resolved.otel, }) logger.withFields({ hostname: resolved.env.HOST, port: resolved.env.PORT }).log('Server started') return { app, injectWebSocket, port: Number(resolved.env.PORT), hostname: resolved.env.HOST, } } function handleProcessError(error: unknown, type: string) { useLogger().withError(error).error(type) } export async function runApiServer(): Promise { const { app: honoApp, injectWebSocket, port, hostname } = await createApp() const server = serve({ fetch: honoApp.fetch, port, hostname }) injectWebSocket(server) process.on('uncaughtException', error => handleProcessError(error, 'Uncaught exception')) process.on('unhandledRejection', error => handleProcessError(error, 'Unhandled rejection')) await new Promise((resolve, reject) => { server.once('close', () => resolve()) server.once('error', error => reject(error)) }) }