diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f881c45a..4aa04ec3bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - **fix(cli-tools):** saving the OpenCode/CLI tool config no longer 400s in cloud mode — every CLI tool card posts `apiKey: null` (the real key is resolved server-side from `keyId`), but `guideSettingsSaveSchema` used `z.string().optional()`, which rejects `null`. The schema now normalizes `null` → `undefined`, so the save succeeds and the `keyId`/default path is used. ([#3552](https://github.com/diegosouzapw/OmniRoute/issues/3552)) - **fix(catalog):** PublicAI is no longer miscatalogued as keyless/free — it requires an API key (registry `authType:"apikey"`; signup grants a one-time credit, then it bills). The three PublicAI models moved from `freeType:"keyless"` (which could pick them into the no-auth pool and dispatch with no `Authorization` header) to `"one-time-initial"`, and the provider's `hasFree` flag is now `false` — matching `freeTierCatalog.ts`, which already excluded publicai. ([#3558](https://github.com/diegosouzapw/OmniRoute/issues/3558)) - **fix(gemini-web):** a missing Playwright Chromium browser no longer loops and trips the provider breaker — when the browser binary is not installed, `chromium.launch()` threw an error surfaced as a retryable **500**, so accountFallback marked the account unavailable and retry-looped. It is now classified as a host/config problem and returns **503** with an actionable message (`npx playwright install chromium`) and the `X-Omni-Fallback-Hint: connection_cooldown` header, which skips the provider circuit breaker and applies a short non-exponential cooldown. ([#3516](https://github.com/diegosouzapw/OmniRoute/issues/3516)) +- **fix(proxy):** the SOCKS5 proxy option now follows the runtime `ENABLE_SOCKS5_PROXY` env instead of the build-time `NEXT_PUBLIC_ENABLE_SOCKS5_PROXY` — Next.js inlines `NEXT_PUBLIC_*` at build time, so a prebuilt Docker image ignored a runtime setting and the SOCKS5 type stayed hidden. The proxy modal now reads `socks5Enabled` from `GET /api/settings/proxies` (server-side `ENABLE_SOCKS5_PROXY`), with the build-time value kept only as a static-deploy fallback. ([#3508](https://github.com/diegosouzapw/OmniRoute/issues/3508)) - **fix(security):** route raw `err.message` through `sanitizeErrorMessage()` in five web executors (`adapta-web`, `deepseek-web`, `perplexity-web`, `qoder`, `veoaifree-web`) and the embeddings + search handlers (Hard Rule #12) — these built error response bodies from the raw upstream/exception message, which could leak internal detail. ([#3494](https://github.com/diegosouzapw/OmniRoute/issues/3494), [#3495](https://github.com/diegosouzapw/OmniRoute/issues/3495)) - **fix(dashboard):** correct two dashboard fetches that hit non-existent routes (404) — `CustomHostsManager` called `/api/tools/traffic-inspector/custom-hosts` (the real route is `/hosts`), and `FeatureFlagsGrid`'s post-restart liveness probe called `/api/health` (the real lightweight endpoint is `/api/health/ping`). ([#3486](https://github.com/diegosouzapw/OmniRoute/issues/3486), [#3487](https://github.com/diegosouzapw/OmniRoute/issues/3487)) - **chore(providers):** remove the dead `krutrim` registry entry — it was half-registered (present in `providerRegistry.ts` with a baseUrl + one model, but absent from `providers.ts`, with no executor/translator/OAuth), so it was never selectable. Dropped its `ProviderIcon` entry and the `KNOWN_REGISTRY_ONLY` exception. ([#3483](https://github.com/diegosouzapw/OmniRoute/issues/3483)) diff --git a/src/app/api/settings/proxies/route.ts b/src/app/api/settings/proxies/route.ts index 11be3f28ca..9d923cf3c2 100644 --- a/src/app/api/settings/proxies/route.ts +++ b/src/app/api/settings/proxies/route.ts @@ -36,7 +36,14 @@ export async function GET(request: Request) { } const proxies = await listProxies({ includeSecrets: false }); - return Response.json({ items: proxies, total: proxies.length }); + // #3508: expose the SOCKS5 feature flag at runtime so the dashboard reflects the live + // ENABLE_SOCKS5_PROXY env (the UI previously gated on NEXT_PUBLIC_*, which is baked at + // build time and ignored a runtime Docker env). + return Response.json({ + items: proxies, + total: proxies.length, + socks5Enabled: process.env.ENABLE_SOCKS5_PROXY === "true", + }); } catch (error) { return createErrorResponseFromUnknown(error, "Failed to load proxies"); } diff --git a/src/shared/components/ProxyConfigModal.tsx b/src/shared/components/ProxyConfigModal.tsx index 83be514d2d..1ec9cb2922 100644 --- a/src/shared/components/ProxyConfigModal.tsx +++ b/src/shared/components/ProxyConfigModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useTranslations } from "next-intl"; import Modal from "./Modal"; import Button from "./Button"; @@ -10,10 +10,12 @@ const ALL_PROXY_TYPES = [ { value: "https", label: "HTTPS" }, { value: "socks5", label: "SOCKS5" }, ]; -const SOCKS5_UI_ENABLED = process.env.NEXT_PUBLIC_ENABLE_SOCKS5_PROXY === "true"; -const PROXY_TYPES = SOCKS5_UI_ENABLED - ? ALL_PROXY_TYPES - : ALL_PROXY_TYPES.filter((type) => type.value !== "socks5"); +// Build-time fallback (static deploys). The live value comes from GET /api/settings/proxies +// (server ENABLE_SOCKS5_PROXY) so a runtime Docker env is honoured — #3508. +const BUILD_TIME_SOCKS5 = process.env.NEXT_PUBLIC_ENABLE_SOCKS5_PROXY === "true"; +export function buildProxyTypes(socks5Enabled: boolean) { + return socks5Enabled ? ALL_PROXY_TYPES : ALL_PROXY_TYPES.filter((type) => type.value !== "socks5"); +} type ProxyConfigLevel = "global" | "provider" | "combo" | "key"; @@ -135,7 +137,9 @@ export default function ProxyConfigModal({ const [mode, setMode] = useState("saved"); const [savedProxies, setSavedProxies] = useState([]); const [selectedProxyId, setSelectedProxyId] = useState(""); - const [proxyType, setProxyType] = useState(PROXY_TYPES[0]?.value || "http"); + const [socks5Enabled, setSocks5Enabled] = useState(BUILD_TIME_SOCKS5); + const proxyTypes = useMemo(() => buildProxyTypes(socks5Enabled), [socks5Enabled]); + const [proxyType, setProxyType] = useState("http"); const [host, setHost] = useState(""); const [port, setPort] = useState(""); const [username, setUsername] = useState(""); @@ -166,14 +170,20 @@ export default function ProxyConfigModal({ try { let hasSavedAssignment = false; let registryItems: ProxyRegistryItem[] = []; + let runtimeSocks5 = BUILD_TIME_SOCKS5; const registryRes = await fetch("/api/settings/proxies"); if (registryRes.ok) { const registryPayload = await registryRes.json(); registryItems = Array.isArray(registryPayload?.items) ? registryPayload.items : []; setSavedProxies(registryItems); + if (typeof registryPayload?.socks5Enabled === "boolean") { + runtimeSocks5 = registryPayload.socks5Enabled; + } } else { setSavedProxies([]); } + setSocks5Enabled(runtimeSocks5); + const runtimeProxyTypes = buildProxyTypes(runtimeSocks5); const scope = getAssignmentScope(level); const assignmentParams = new URLSearchParams({ scope }); @@ -194,9 +204,9 @@ export default function ProxyConfigModal({ const assignedProxy = registryItems.find((item) => item.id === target.proxyId); if (assignedProxy?.source === DASHBOARD_CUSTOM_PROXY_SOURCE) { const normalizedType = String(assignedProxy.type || "http").toLowerCase(); - const hasTypeOption = PROXY_TYPES.some((entry) => entry.value === normalizedType); + const hasTypeOption = runtimeProxyTypes.some((entry) => entry.value === normalizedType); setMode("custom"); - setProxyType(hasTypeOption ? normalizedType : PROXY_TYPES[0]?.value || "http"); + setProxyType(hasTypeOption ? normalizedType : runtimeProxyTypes[0]?.value || "http"); setHost(assignedProxy.host || ""); setPort(String(assignedProxy.port || "")); setUsername( @@ -206,7 +216,7 @@ export default function ProxyConfigModal({ isRedactedSecret(assignedProxy.password) ? "" : assignedProxy.password || "" ); setShowAuth(!!(assignedProxy.username || assignedProxy.password)); - if (normalizedType === "socks5" && !SOCKS5_UI_ENABLED) { + if (normalizedType === "socks5" && !runtimeSocks5) { setFormError(t("errorSocks5Hidden")); } } else { @@ -227,15 +237,15 @@ export default function ProxyConfigModal({ const proxy = data.proxy; if (proxy && proxy.host) { const normalizedType = String(proxy.type || "http").toLowerCase(); - const hasTypeOption = PROXY_TYPES.some((entry) => entry.value === normalizedType); - setProxyType(hasTypeOption ? normalizedType : PROXY_TYPES[0]?.value || "http"); + const hasTypeOption = runtimeProxyTypes.some((entry) => entry.value === normalizedType); + setProxyType(hasTypeOption ? normalizedType : runtimeProxyTypes[0]?.value || "http"); setHost(proxy.host || ""); setPort(proxy.port || ""); setUsername(proxy.username || ""); setPassword(proxy.password || ""); setShowAuth(!!(proxy.username || proxy.password)); setHasOwnProxy(true); - if (normalizedType === "socks5" && !SOCKS5_UI_ENABLED) { + if (normalizedType === "socks5" && !runtimeSocks5) { setFormError(t("errorSocks5Hidden")); } if (!hasSavedAssignment) setMode("custom"); @@ -279,7 +289,7 @@ export default function ProxyConfigModal({ }, [isOpen, level, levelId]); const resetFields = () => { - setProxyType(PROXY_TYPES[0]?.value || "http"); + setProxyType(proxyTypes[0]?.value || "http"); setHost(""); setPort(""); setUsername(""); @@ -602,7 +612,7 @@ export default function ProxyConfigModal({ {t("proxyType")}
- {PROXY_TYPES.map((t) => ( + {proxyTypes.map((t) => (