| |
| |
| |
| |
| |
| import { NextRequest, NextResponse } from 'next/server'; |
| import { webRequestPreflight } from '@/lib/web/request-guards'; |
| import { assertPublicUrl } from '@/lib/web/ssrf-guard'; |
| import { getWebSearchProvider, WEB_SEARCH_PROVIDERS } from '@/lib/web-search'; |
| import type { WebSearchProviderId } from '@/lib/web-search'; |
|
|
| export const runtime = 'nodejs'; |
|
|
| export async function POST(request: NextRequest) { |
| const pre = webRequestPreflight(request); |
| if (!pre.ok) return pre.response; |
|
|
| let payload: { provider?: string; query?: string; count?: number; markdown?: boolean; auth?: { key?: string; searxngUrl?: string } }; |
| try { payload = await request.json(); } |
| catch { return NextResponse.json({ error: 'invalid request body' }, { status: 400 }); } |
|
|
| const { provider: providerId, query, count, markdown, auth = {} } = payload; |
| if (!query || typeof query !== 'string') { |
| return NextResponse.json({ error: 'missing query' }, { status: 400 }); |
| } |
| if (!providerId || !(providerId in WEB_SEARCH_PROVIDERS)) { |
| return NextResponse.json({ error: 'unknown or unconfigured search provider' }, { status: 400 }); |
| } |
| const provider = getWebSearchProvider(providerId as WebSearchProviderId); |
|
|
| |
| if (provider.auth === 'key' && !auth.key) { |
| return NextResponse.json({ error: `search provider ${provider.name} requires an API key` }, { status: 400 }); |
| } |
| if (provider.auth === 'url' && !auth.searxngUrl) { |
| return NextResponse.json({ error: `search provider ${provider.name} requires an instance URL` }, { status: 400 }); |
| } |
|
|
| try { |
| const { url, init } = provider.buildRequest(query, { count, markdown }, auth); |
| |
| |
| if (provider.auth === 'url') { |
| await assertPublicUrl(url); |
| } |
| const resp = await fetch(url, { ...init, signal: AbortSignal.timeout(20_000) }); |
| if (!resp.ok) { |
| const text = await resp.text().catch(() => ''); |
| return NextResponse.json({ error: `provider error (${provider.name}): ${resp.status} ${text.slice(0, 200)}` }, { status: 200 }); |
| } |
| const raw = await resp.json(); |
| const results = provider.normalize(raw); |
| return NextResponse.json({ results }); |
| } catch (e: unknown) { |
| const msg = e instanceof Error ? (e.name === 'TimeoutError' ? 'timeout after 20s' : e.message) : 'search failed'; |
| return NextResponse.json({ error: `provider error (${provider.name}): ${msg}` }, { status: 200 }); |
| } |
| } |
|
|