diff --git a/apps/server/convex/users.ts b/apps/server/convex/users.ts index 512757a6..4752e955 100644 --- a/apps/server/convex/users.ts +++ b/apps/server/convex/users.ts @@ -249,6 +249,11 @@ export const getByExternalId = query({ profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, ======= <<<<<<< HEAD +||||||| 54e09ce + encryptedOpenRouterKey: + profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, +======= +<<<<<<< HEAD ||||||| 54e09ce encryptedOpenRouterKey: profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, @@ -256,6 +261,7 @@ export const getByExternalId = query({ hasOpenRouterKey: !!(profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey), >>>>>>> main >>>>>>> main +>>>>>>> main >>>>>>> main fileUploadCount: profile?.fileUploadCount ?? user.fileUploadCount ?? 0, aiUsageCents: user.aiUsageCents, @@ -345,6 +351,11 @@ export const getByExternalIdInternal = internalQuery({ profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, ======= <<<<<<< HEAD +||||||| 54e09ce + encryptedOpenRouterKey: + profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, +======= +<<<<<<< HEAD ||||||| 54e09ce encryptedOpenRouterKey: profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey, @@ -352,6 +363,7 @@ export const getByExternalIdInternal = internalQuery({ hasOpenRouterKey: !!(profile?.encryptedOpenRouterKey ?? user.encryptedOpenRouterKey), >>>>>>> main >>>>>>> main +>>>>>>> main >>>>>>> main fileUploadCount: profile?.fileUploadCount ?? user.fileUploadCount ?? 0, aiUsageCents: user.aiUsageCents, diff --git a/apps/web/src/routes/api/models.ts b/apps/web/src/routes/api/models.ts index 2353f3a4..50da446e 100644 --- a/apps/web/src/routes/api/models.ts +++ b/apps/web/src/routes/api/models.ts @@ -76,6 +76,95 @@ async function fetchModelsFromOpenRouter(): Promise { } } +function getClientIp(request: Request): string | null { + if (!TRUST_PROXY_MODE) { + return null; + } + + if (TRUST_PROXY_MODE === "cloudflare") { + const cfConnectingIp = request.headers.get("cf-connecting-ip")?.trim(); + return cfConnectingIp || null; + } + + if (TRUST_PROXY_MODE === "vercel") { + const vercelForwardedFor = request.headers.get("x-vercel-forwarded-for")?.trim(); + if (vercelForwardedFor) { + const first = vercelForwardedFor.split(",")[0]?.trim(); + if (first) return first; + } + return null; + } + + if (TRUST_PROXY_MODE === "true") { +||||||| 54e09ce +======= +<<<<<<< HEAD +if (TRUST_PROXY_MODE === "true") { + console.warn("[Models API] TRUST_PROXY=true requires x-forwarded-for for rate limiting"); +} + +if (!TRUST_PROXY_MODE) { + console.warn("[Models API] TRUST_PROXY is unset; models endpoint will reject requests when IP is unavailable"); +} + +if ( + TRUST_PROXY_MODE && + TRUST_PROXY_MODE !== "cloudflare" && + TRUST_PROXY_MODE !== "vercel" && + TRUST_PROXY_MODE !== "true" +) { + console.warn("[Models API] Unrecognized TRUST_PROXY value; models endpoint will reject requests when IP is unavailable"); +} + +const modelsIpRatelimit = upstashRedis + ? new Ratelimit({ + redis: upstashRedis, + limiter: Ratelimit.slidingWindow(30, "60 s"), + prefix: "ratelimit:models:ip", + }) + : null; + +async function fetchModelsFromOpenRouter(): Promise { + try { + const response = await fetch(OPENROUTER_MODELS_URL, { + headers: { + Accept: "application/json", + }, + signal: AbortSignal.timeout(OPENROUTER_FETCH_TIMEOUT_MS), + }); + + if (!response.ok) { + return json( + { error: "Upstream service error" }, + { status: 502 }, + ); + } + + const payload = await response.text(); + + if (upstashRedis) { + try { + await upstashRedis.set(MODELS_CACHE_KEY, payload, { + ex: MODELS_CACHE_TTL_SECONDS, + }); + } catch (error) { + console.warn("[Models API] Failed to write cache:", error); + } + } + + return new Response(payload, { + status: 200, + headers: { + "Content-Type": "application/json", + "Cache-Control": "no-store", + }, + }); + } catch (error) { + console.warn("[Models API] OpenRouter fetch failed:", error); + return json({ error: "Upstream service unavailable" }, { status: 502 }); + } +} + function getClientIp(request: Request): string | null { if (!TRUST_PROXY_MODE) { return null; @@ -308,6 +397,7 @@ function getClientIp(request: Request): string | null { if (TRUST_PROXY_MODE === "true") { >>>>>>> main +>>>>>>> main >>>>>>> main const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); if (forwardedFor) { diff --git a/apps/web/src/stores/stream.ts b/apps/web/src/stores/stream.ts index bf7096c3..20b30749 100644 --- a/apps/web/src/stores/stream.ts +++ b/apps/web/src/stores/stream.ts @@ -204,10 +204,32 @@ export const useStreamStore = create()( }), { name: "openchat-stream", - storage: createJSONStorage(() => localStorage), + storage: createJSONStorage(() => sessionStorage), partialize: (state) => ({ - activeStream: state.activeStream, - pendingUserMessage: state.pendingUserMessage, + // Persist only non-sensitive metadata for stream resumption. + // Sensitive fields (content, reasoning, text) are excluded to + // prevent exposure via XSS or local profile compromise. + activeStream: state.activeStream + ? { + chatId: state.activeStream.chatId, + messageId: state.activeStream.messageId, + streamId: state.activeStream.streamId, + lastEventId: state.activeStream.lastEventId, + content: "", + reasoning: "", + startedAt: state.activeStream.startedAt, + } + : null, + pendingUserMessage: state.pendingUserMessage + ? { + chatId: state.pendingUserMessage.chatId, + messageId: state.pendingUserMessage.messageId, + text: "", + createdAt: state.pendingUserMessage.createdAt, + mode: state.pendingUserMessage.mode, + resumeAt: state.pendingUserMessage.resumeAt, + } + : null, }), },