route.ts
2,013 bytes
| 1 | import { NextResponse } from 'next/server'; |
|---|---|
| 2 | import { currentUserId } from '@/auth'; |
| 3 | import { resolveDebateConfig } from '@/core/config'; |
| 4 | import { debateConfigInputSchema } from '@/core/schemas'; |
| 5 | import { KeyResolutionError, startDebateStream } from '@/lib/debate-runner'; |
| 6 | import { env } from '@/lib/env'; |
| 7 | import { rateLimit } from '@/lib/rate-limit'; |
| 8 | |
| 9 | export const runtime = 'nodejs'; |
| 10 | export const dynamic = 'force-dynamic'; |
| 11 | // Debate streams run for minutes; on a self-hosted node server there is no cap, |
| 12 | // but declare a generous ceiling for platforms that honor it. |
| 13 | export const maxDuration = 800; |
| 14 | |
| 15 | function clientIp(request: Request): string { |
| 16 | const fwd = request.headers.get('x-forwarded-for'); |
| 17 | return fwd?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || 'unknown'; |
| 18 | } |
| 19 | |
| 20 | export async function POST(request: Request) { |
| 21 | const body = await request.json().catch(() => null); |
| 22 | const parsed = debateConfigInputSchema.safeParse(body); |
| 23 | if (!parsed.success) { |
| 24 | return NextResponse.json({ error: 'Invalid debate configuration', issues: parsed.error.flatten() }, { status: 400 }); |
| 25 | } |
| 26 | const config = resolveDebateConfig(parsed.data); |
| 27 | const userId = await currentUserId(); |
| 28 | |
| 29 | const rlKey = userId ? `user:${userId}` : `ip:${clientIp(request)}`; |
| 30 | const rl = rateLimit(`debate:${rlKey}`, env.RATE_LIMIT_DEBATES_PER_HOUR, 60 * 60 * 1000); |
| 31 | if (!rl.allowed) { |
| 32 | return NextResponse.json( |
| 33 | { error: `Rate limit reached (${rl.limit}/hour). Try again in ${Math.ceil(rl.resetMs / 60000)} min.` }, |
| 34 | { status: 429, headers: { 'Retry-After': String(Math.ceil(rl.resetMs / 1000)) } }, |
| 35 | ); |
| 36 | } |
| 37 | |
| 38 | try { |
| 39 | const { response } = await startDebateStream({ config, userId }); |
| 40 | return response; |
| 41 | } catch (err) { |
| 42 | if (err instanceof KeyResolutionError) { |
| 43 | return NextResponse.json({ error: err.message, code: 'NO_KEY' }, { status: 401 }); |
| 44 | } |
| 45 | console.error('debate start failed', err); |
| 46 | return NextResponse.json({ error: 'Failed to start debate' }, { status: 500 }); |
| 47 | } |
| 48 | } |
| 49 | |