(request: Request)
| 126 | * See https://vercel.com/changelog/node-js-vercel-functions-now-support-fetch-web-handlers |
| 127 | */ |
| 128 | const fetch = async (request: Request): Promise<Response> => { |
| 129 | if (request.method === "OPTIONS") { |
| 130 | // We'll always serve this same-origin so we don't need any CORS config |
| 131 | return new Response(null, { status: 204 }); |
| 132 | } |
| 133 | |
| 134 | if (request.method !== "POST") { |
| 135 | logChatFailure("Rejected unsupported method", { method: request.method }); |
| 136 | return jsonResponse({ error: "Method not allowed" }, { status: 405 }); |
| 137 | } |
| 138 | |
| 139 | const clientIp = resolveClientIp(request); |
| 140 | if (process.env.VERCEL_ENV === "production" && !clientIp) { |
| 141 | // Vercel's edge always sets x-forwarded-for in production. If it isn't |
| 142 | // present, the request reached us through an unexpected path and we have |
| 143 | // no way to rate-limit it - reject conservatively rather than fail open. |
| 144 | logChatFailure("Rejected production request with no resolvable client IP"); |
| 145 | return jsonResponse( |
| 146 | { error: "Could not determine client IP" }, |
| 147 | { status: 400 }, |
| 148 | ); |
| 149 | } |
| 150 | |
| 151 | if (clientIp && !checkRateLimit(clientIp)) { |
| 152 | logChatFailure("Rejected rate-limited request", { clientIp }); |
| 153 | return jsonResponse({ error: "Rate limit exceeded" }, { status: 429 }); |
| 154 | } |
| 155 | |
| 156 | const apiKey = process.env.OPENAI_API_KEY; |
| 157 | if (!apiKey) { |
| 158 | logChatFailure("Missing OpenAI API key"); |
| 159 | return jsonResponse( |
| 160 | { error: "OPENAI_API_KEY is not configured" }, |
| 161 | { status: 500 }, |
| 162 | ); |
| 163 | } |
| 164 | |
| 165 | let body: unknown; |
| 166 | try { |
| 167 | body = await request.json(); |
| 168 | } catch (error) { |
| 169 | logChatFailure("Rejected invalid JSON", { error }); |
| 170 | return jsonResponse({ error: "Invalid JSON" }, { status: 400 }); |
| 171 | } |
| 172 | |
| 173 | const parsed = requestSchema.safeParse(body); |
| 174 | if (!parsed.success) { |
| 175 | logChatFailure("Rejected invalid chat request", { error: parsed.error }); |
| 176 | return jsonResponse({ error: "Invalid chat request" }, { status: 400 }); |
| 177 | } |
| 178 | |
| 179 | const validatedMessages = await safeValidateUIMessages<UIMessage>({ |
| 180 | messages: parsed.data.messages, |
| 181 | tools: petrinautAiValidationTools, |
| 182 | }); |
| 183 | |
| 184 | if (!validatedMessages.success) { |
| 185 | logChatFailure("Rejected invalid chat messages", { |
no test coverage detected