diff --git a/apps/web/src/components/app-sidebar.tsx b/apps/web/src/components/app-sidebar.tsx index 334666be..5bc1408f 100644 --- a/apps/web/src/components/app-sidebar.tsx +++ b/apps/web/src/components/app-sidebar.tsx @@ -283,15 +283,9 @@ export function AppSidebar({ if (chatsResult?.chats && chatsResult.chats.length > 0) { cachedChatsRef.current = chatsResult.chats; try { -<<<<<<< HEAD - sessionStorage.setItem(CHATS_CACHE_KEY, JSON.stringify(chatsResult.chats)); -||||||| 54e09ce - localStorage.setItem(CHATS_CACHE_KEY, JSON.stringify(chatsResult.chats)); -======= // Only cache minimal fields needed for sidebar rendering const minimal = chatsResult.chats.map(({ _id, title, updatedAt }) => ({ _id, title, updatedAt })); sessionStorage.setItem(CHATS_CACHE_KEY, JSON.stringify(minimal)); ->>>>>>> main } catch (e) { console.warn("Failed to save chats to sessionStorage:", e); } diff --git a/apps/web/src/lib/auth-client.tsx b/apps/web/src/lib/auth-client.tsx index 5b2d8ba4..baf1f9d2 100644 --- a/apps/web/src/lib/auth-client.tsx +++ b/apps/web/src/lib/auth-client.tsx @@ -1,5 +1,5 @@ import { - + createContext, useCallback, useContext, @@ -16,7 +16,6 @@ import { env } from "./env"; import { analytics } from "./analytics"; import type {ReactNode} from "react"; - /** * Better Auth client with Convex integration. */ diff --git a/apps/web/src/routes/api/models.ts b/apps/web/src/routes/api/models.ts index f89fef7e..879c67b6 100644 --- a/apps/web/src/routes/api/models.ts +++ b/apps/web/src/routes/api/models.ts @@ -9,7 +9,6 @@ const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"; const OPENROUTER_FETCH_TIMEOUT_MS = 10_000; const TRUST_PROXY_MODE = process.env.TRUST_PROXY?.trim().toLowerCase(); -<<<<<<< HEAD /** * Basic IPv4/IPv6 format validation. * Rejects obviously spoofed or malformed values used in x-forwarded-for. @@ -132,712 +131,6 @@ function getClientIp(request: Request): string | null { if (forwardedFor) { const first = forwardedFor.split(",")[0]?.trim(); if (first && isValidIpFormat(first)) return first; -||||||| 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; - } - - 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") { - const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); - if (forwardedFor) { - const first = forwardedFor.split(",")[0]?.trim(); - if (first) return first; -||||||| 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; - } - - 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") { - const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); - if (forwardedFor) { - const first = forwardedFor.split(",")[0]?.trim(); - if (first) return first; -||||||| 54e09ce -======= -<<<<<<< HEAD -// Basic IPv4 and IPv6 validation to reject obviously spoofed or malformed values. -const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; -const IPV6_REGEX = /^[\da-fA-F:]+$/; - -function isValidIp(value: string): boolean { - if (IPV4_REGEX.test(value)) return true; - // Rough IPv6 check: only hex digits and colons, reasonable length - if (IPV6_REGEX.test(value) && value.includes(":") && value.length <= 45) return true; - return false; -} - -if (TRUST_PROXY_MODE === "true") { - console.warn( - "[Models API] TRUST_PROXY=true blindly trusts X-Forwarded-For and is vulnerable to " + - "IP spoofing if not behind a trusted proxy. Prefer TRUST_PROXY=cloudflare or " + - "TRUST_PROXY=vercel for platform-specific secure headers.", - ); -} - -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; - } - - if (TRUST_PROXY_MODE === "cloudflare") { - const cfConnectingIp = request.headers.get("cf-connecting-ip")?.trim(); - if (cfConnectingIp && isValidIp(cfConnectingIp)) return cfConnectingIp; - return 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 && isValidIp(first)) return first; - } - return null; - } - - if (TRUST_PROXY_MODE === "true") { - // Prefer platform-specific headers that are harder to spoof, then fall - // back to the generic X-Forwarded-For only if none are present. - const cfConnectingIp = request.headers.get("cf-connecting-ip")?.trim(); - if (cfConnectingIp && isValidIp(cfConnectingIp)) return cfConnectingIp; - - const vercelForwardedFor = request.headers.get("x-vercel-forwarded-for")?.trim(); - if (vercelForwardedFor) { - const first = vercelForwardedFor.split(",")[0]?.trim(); - if (first && isValidIp(first)) return first; - } - - const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); - if (forwardedFor) { - const first = forwardedFor.split(",")[0]?.trim(); - if (first && isValidIp(first)) return first; -||||||| 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; - } - - 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; - } - - 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 -/** - * TRUSTED_PROXIES: comma-separated list of trusted proxy IPs. - * Required when TRUST_PROXY=true to prevent x-forwarded-for spoofing. - * When set, only x-forwarded-for values from requests are accepted if - * the deployment is explicitly configured to trust the reverse proxy chain. - * Platform-specific modes (cloudflare, vercel) use tamper-resistant headers - * and do not require this setting. - */ -const TRUSTED_PROXY_IPS: ReadonlySet = new Set( - (process.env.TRUSTED_PROXIES ?? "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean), -); - -if (TRUST_PROXY_MODE === "true" && TRUSTED_PROXY_IPS.size === 0) { - console.warn( - "[Models API] SECURITY WARNING: TRUST_PROXY=true without TRUSTED_PROXIES is unsafe. " + - "The x-forwarded-for header can be spoofed by clients. " + - "Set TRUSTED_PROXIES to a comma-separated list of trusted proxy IPs, " + - "or use a platform-specific mode (cloudflare, vercel). " + - "Rate limiting will fall back to rejecting requests when client IP cannot be verified.", - ); -} - -if (TRUST_PROXY_MODE === "true" && TRUSTED_PROXY_IPS.size > 0) { - console.info( - `[Models API] TRUST_PROXY=true with ${TRUSTED_PROXY_IPS.size} trusted proxy IP(s) configured`, - ); -} - -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; - } - - 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") { - // Without TRUSTED_PROXIES configured, x-forwarded-for is spoofable. - // Fail closed: return null so the request is rejected with a 400, - // preventing rate-limit bypass via header spoofing. - if (TRUSTED_PROXY_IPS.size === 0) { - return null; - } - -||||||| 54e09ce -======= -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; - } - - 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") { ->>>>>>> main ->>>>>>> main ->>>>>>> main - const forwardedFor = request.headers.get("x-forwarded-for")?.trim(); - if (forwardedFor) { - const first = forwardedFor.split(",")[0]?.trim(); - if (first) return first; ->>>>>>> main ->>>>>>> main ->>>>>>> main ->>>>>>> main } return null; } diff --git a/apps/web/src/stores/prompt-draft.ts b/apps/web/src/stores/prompt-draft.ts index c075f2a6..dfae0fb0 100644 --- a/apps/web/src/stores/prompt-draft.ts +++ b/apps/web/src/stores/prompt-draft.ts @@ -4,29 +4,15 @@ import { createJSONStorage, devtools, persist } from "zustand/middleware"; /** * Store for persisting prompt drafts within the current browser session. * -<<<<<<< HEAD - * Uses sessionStorage instead of localStorage to limit exposure of sensitive - * chat content — data is scoped to the tab/session and not accessible after - * the browser session ends. -||||||| 54e09ce - * Store for persisting prompt drafts across page reloads. -======= * Security: Uses sessionStorage instead of localStorage to limit exposure * of sensitive draft content. Drafts are automatically cleared when the * browser tab is closed, reducing the risk of exfiltration via XSS or * compromised browser profiles. ->>>>>>> main * * Non-annoying approach: * - Drafts are saved per-chat (or "global" for new chat input) * - Drafts are automatically cleared when a message is sent -<<<<<<< HEAD - * - Old drafts are cleaned up after 7 days to prevent storage bloat -||||||| 54e09ce - * - Old drafts are cleaned up after 7 days to prevent localStorage bloat -======= * - Old drafts are cleaned up after 24 hours as a defensive measure ->>>>>>> main */ const DRAFT_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours (session-scoped, defensive expiry) @@ -91,7 +77,7 @@ export const usePromptDraftStore = create()( return ""; } - // Don't return expired drafts + // Only return drafts that have not yet expired if (Date.now() - draft.updatedAt < DRAFT_EXPIRY_MS) { return draft.text; } diff --git a/apps/web/src/stores/stream.ts b/apps/web/src/stores/stream.ts index 20b30749..f285a26b 100644 --- a/apps/web/src/stores/stream.ts +++ b/apps/web/src/stores/stream.ts @@ -166,40 +166,40 @@ export const useStreamStore = create()( "stream/error", ), - setResuming: () => set({ status: "resuming" }, false, "stream/resuming"), - - - reset: () => set(initialState, false, "stream/reset"), - - setPendingUserMessage: (message) => - set({ pendingUserMessage: message }, false, "stream/pending/set"), - - consumePendingUserMessage: (chatId: string) => { - const pending = get().pendingUserMessage; - if (pending && pending.chatId === chatId) { - set({ pendingUserMessage: null }, false, "stream/pending/consume"); - return pending; - } - return null; - }, - - clearPendingUserMessage: (chatId: string) => { - const pending = get().pendingUserMessage; - if (pending && pending.chatId === chatId) { - set({ pendingUserMessage: null }, false, "stream/pending/clear"); - } - }, - - getActiveStreamForChat: (chatId: string) => { - const stream = get().activeStream; - if (!stream) return null; - if (stream.chatId !== chatId) return null; - if (Date.now() - stream.startedAt > 10 * 60 * 1000) { - set({ activeStream: null }, false, "stream/expired"); - return null; - } - return stream; - }, + setResuming: () => set({ status: "resuming" }, false, "stream/resuming"), + + + reset: () => set(initialState, false, "stream/reset"), + + setPendingUserMessage: (message) => + set({ pendingUserMessage: message }, false, "stream/pending/set"), + + consumePendingUserMessage: (chatId: string) => { + const pending = get().pendingUserMessage; + if (pending && pending.chatId === chatId) { + set({ pendingUserMessage: null }, false, "stream/pending/consume"); + return pending; + } + return null; + }, + + clearPendingUserMessage: (chatId: string) => { + const pending = get().pendingUserMessage; + if (pending && pending.chatId === chatId) { + set({ pendingUserMessage: null }, false, "stream/pending/clear"); + } + }, + + getActiveStreamForChat: (chatId: string) => { + const stream = get().activeStream; + if (!stream) return null; + if (stream.chatId !== chatId) return null; + if (Date.now() - stream.startedAt > 10 * 60 * 1000) { + set({ activeStream: null }, false, "stream/expired"); + return null; + } + return stream; + }, }), {