import 'dotenv/config' import express from 'express' import type { Request, Response, NextFunction } from 'express' import path from 'path' import { auth } from './middleware/auth.js' import { logger } from './logger.js' import { authRouter } from './services/auth/auth.routes' import { chatRouter } from './services/chat/chat.routes' import { expenseRouter } from './services/expense/router' import { workspaceMembersRouter } from './services/expense/workspace-members/workspace-members.routes' import { slipQueueRouter } from './services/expense/slip-queue/slip-queue.routes' import { publicRouter } from './services/expense/public/public.routes' import { healthRouter } from './health/health.routes' import { runMigrations } from './services/expense/db/migrate.js' import { seedDefaults } from './services/expense/db/seed.js' const app = express() app.use(express.json({ limit: '50mb' })) app.use(express.urlencoded({ limit: '50mb', extended: true })) app.use(express.raw({ limit: '50mb', type: ['image/*', 'application/octet-stream'] })) app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*') res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS') res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-internal-key') if (req.method === 'OPTIONS') { res.sendStatus(204); return } next() }) app.use(authRouter) // /auth/login is public // Serve built dashboard — no auth needed for static assets or SPA routes const dashboardDist = path.join(__dirname, '../../web/dist') app.use(express.static(dashboardDist)) app.get(/^\/(?!v1\/|auth\/|health)/, (_req, res) => { res.sendFile(path.join(dashboardDist, 'index.html')) }) app.use('/v1/public', publicRouter) // token-scoped, no auth — the token itself is the access control app.use(auth) // all API routes require auth (internal-secret bypass, else JWT via auth-client) app.use(healthRouter) app.use('/v1', chatRouter) app.use('/v1/expense', expenseRouter) app.use('/v1/workspaces', workspaceMembersRouter) app.use('/v1/slip-queue', slipQueueRouter) app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => { logger.error('unhandled_route_error', { method: req.method, path: req.path, error: err instanceof Error ? err.message : String(err), cause: err instanceof Error && err.cause instanceof Error ? err.cause.message : undefined, stack: err instanceof Error ? err.stack : undefined, }) if (res.headersSent) return res.status(500).json({ error: 'Internal server error' }) }) const PORT = parseInt(process.env['PORT'] ?? '7860', 10) const FOUR_MINUTES = 4 * 60 * 1000 async function keepAlive(): Promise { const services = [ `http://localhost:${PORT}/health`, `${process.env['OLLAMA_HOST'] ?? ''}/api/tags`, ] for (const url of services) { try { const pingRes = await fetch(url, { headers: { 'x-internal-key': process.env['INTERNAL_SECRET'] ?? '' }, signal: AbortSignal.timeout(5000), }) console.log('keepalive_ping', { url, status: pingRes.status }) } catch (err: unknown) { console.warn('keepalive_failed', { url, error: err instanceof Error ? err.message : String(err) }) } } } setInterval(() => void keepAlive(), FOUR_MINUTES) void keepAlive() async function start(): Promise { await runMigrations() await seedDefaults() app.listen(PORT, '0.0.0.0', () => console.log(`Proxy running on port ${PORT}`)) } start().catch((err: unknown) => { console.error('Failed to start server:', err) process.exit(1) })