From 8077e9b62bfdedb13d52042a3169bd40573bb2c8 Mon Sep 17 00:00:00 2001 From: Sergey Morozov Date: Tue, 5 May 2026 15:12:35 +0300 Subject: [PATCH] feat(settings): add request body limit setting (#1968) Integrated into release/v3.7.9 --- .../settings/components/ProxyTab.tsx | 2 + .../settings/components/RequestLimitsTab.tsx | 167 ++++++++++++++++++ .../settings/components/SystemStorageTab.tsx | 8 +- src/app/api/usage/analytics/route.ts | 14 ++ src/i18n/messages/ar.json | 18 +- src/i18n/messages/bg.json | 16 ++ src/i18n/messages/bn.json | 18 +- src/i18n/messages/cs.json | 16 ++ src/i18n/messages/da.json | 18 +- src/i18n/messages/de.json | 18 +- src/i18n/messages/en.json | 16 ++ src/i18n/messages/es.json | 18 +- src/i18n/messages/fa.json | 18 +- src/i18n/messages/fi.json | 16 ++ src/i18n/messages/fr.json | 18 +- src/i18n/messages/gu.json | 18 +- src/i18n/messages/he.json | 16 ++ src/i18n/messages/hi.json | 16 ++ src/i18n/messages/hu.json | 16 ++ src/i18n/messages/id.json | 16 ++ src/i18n/messages/in.json | 18 +- src/i18n/messages/it.json | 18 +- src/i18n/messages/ja.json | 16 ++ src/i18n/messages/ko.json | 16 ++ src/i18n/messages/mr.json | 18 +- src/i18n/messages/ms.json | 18 +- src/i18n/messages/nl.json | 16 ++ src/i18n/messages/no.json | 18 +- src/i18n/messages/phi.json | 16 ++ src/i18n/messages/pl.json | 18 +- src/i18n/messages/pt-BR.json | 16 ++ src/i18n/messages/pt.json | 18 +- src/i18n/messages/ro.json | 16 ++ src/i18n/messages/ru.json | 16 ++ src/i18n/messages/sk.json | 16 ++ src/i18n/messages/sv.json | 16 ++ src/i18n/messages/sw.json | 18 +- src/i18n/messages/ta.json | 18 +- src/i18n/messages/te.json | 18 +- src/i18n/messages/th.json | 18 +- src/i18n/messages/tr.json | 16 ++ src/i18n/messages/uk-UA.json | 16 ++ src/i18n/messages/ur.json | 18 +- src/i18n/messages/vi.json | 18 +- src/i18n/messages/zh-CN.json | 16 ++ src/lib/db/core.ts | 10 ++ src/lib/db/encryption.ts | 81 ++++++++- src/lib/db/providers.ts | 67 ++++++- src/lib/db/settings.ts | 2 + src/lib/usage/migrations.ts | 6 +- src/server/authz/pipeline.ts | 19 +- src/shared/constants/bodySize.ts | 39 ++++ src/shared/middleware/bodySizeGuard.ts | 27 ++- src/shared/validation/schemas.ts | 7 + src/shared/validation/settingsSchemas.ts | 109 +++++++++++- src/types/settings.ts | 1 + tests/unit/body-size-guard.test.ts | 42 +++++ tests/unit/settings-i18n-keys.test.ts | 79 +++++++++ ...settings-schema-routing-strategies.test.ts | 12 ++ 59 files changed, 1352 insertions(+), 38 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/settings/components/RequestLimitsTab.tsx create mode 100644 src/shared/constants/bodySize.ts create mode 100644 tests/unit/body-size-guard.test.ts diff --git a/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx b/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx index 6925fa57d0..355f2d1909 100644 --- a/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/ProxyTab.tsx @@ -5,6 +5,7 @@ import { Card, Button, ProxyConfigModal, Toggle } from "@/shared/components"; import { useTranslations } from "next-intl"; import ProxyRegistryManager from "./ProxyRegistryManager"; import OneproxyTab from "./OneproxyTab"; +import RequestLimitsTab from "./RequestLimitsTab"; export default function ProxyTab() { const [proxyModalOpen, setProxyModalOpen] = useState(false); @@ -184,6 +185,7 @@ export default function ProxyTab() { + (null); + + useEffect(() => { + let active = true; + + fetch("/api/settings") + .then((response) => { + if (!response.ok) throw new Error(`Settings API returned ${response.status}`); + return response.json() as Promise; + }) + .then((settings) => { + if (!active) return; + const nextValue = normalizeInputValue(settings.maxBodySizeMb); + setValue(nextValue); + setSavedValue(nextValue); + }) + .catch((error) => { + console.error("Failed to load request limit settings:", error); + if (active) { + setMessage({ type: "error", text: t("requestBodyLimitLoadFailed") }); + } + }) + .finally(() => { + if (active) setLoading(false); + }); + + return () => { + active = false; + }; + }, [t]); + + const validationError = useMemo(() => { + const trimmed = value.trim(); + if (!trimmed) return t("requestBodyLimitEmptyError"); + + const parsed = Number(trimmed); + if (!Number.isInteger(parsed)) return t("requestBodyLimitWholeNumberError"); + if (parsed < MIN_REQUEST_BODY_LIMIT_MB) { + return t("requestBodyLimitMinimumError", { min: MIN_REQUEST_BODY_LIMIT_MB }); + } + if (parsed > MAX_REQUEST_BODY_LIMIT_MB) { + return t("requestBodyLimitMaximumError", { max: MAX_REQUEST_BODY_LIMIT_MB }); + } + + return null; + }, [t, value]); + + const dirty = value.trim() !== savedValue; + + const saveLimit = useCallback(async () => { + if (validationError || !dirty) return; + + const nextValue = Number(value.trim()); + setSaving(true); + setMessage(null); + + try { + const response = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ maxBodySizeMb: nextValue }), + }); + + if (!response.ok) throw new Error(`Settings API returned ${response.status}`); + + const settings = (await response.json()) as SettingsResponse; + const saved = normalizeInputValue(settings.maxBodySizeMb ?? nextValue); + setValue(saved); + setSavedValue(saved); + setMessage({ type: "success", text: t("requestBodyLimitSaveSuccess") }); + } catch (error) { + console.error("Failed to save request body limit:", error); + setMessage({ type: "error", text: t("requestBodyLimitSaveFailed") }); + } finally { + setSaving(false); + } + }, [dirty, t, validationError, value]); + + return ( + +
+
+

{t("requestBodyLimitTitle")}

+

{t("requestBodyLimitDescription")}

+
+
+ + { + setValue(event.target.value); + setMessage(null); + }} + onKeyDown={(event) => { + if (event.key === "Enter" && dirty) void saveLimit(); + }} + className="w-32 px-3 py-1.5 rounded bg-surface-2 border border-border text-sm text-text-primary" + disabled={loading || saving} + /> + MB + + {dirty && ( + + {t("requestBodyLimitCurrent", { value: savedValue })} + + )} +
+ {validationError &&

{validationError}

} + {message && ( +

+ {message.text} +

+ )} +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx index 1adc5c432f..93fd27e9ac 100644 --- a/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx @@ -4,6 +4,12 @@ import { useState, useEffect, useRef } from "react"; import { Card, Button, Badge } from "@/shared/components"; import { useLocale, useTranslations } from "next-intl"; +const rowCountFormatter = new Intl.NumberFormat("en-US"); + +function formatRows(rows: number | null | undefined) { + return typeof rows === "number" ? rowCountFormatter.format(rows) : "100K"; +} + export default function SystemStorageTab() { const [backups, setBackups] = useState([]); const [backupsLoading, setBackupsLoading] = useState(false); @@ -588,7 +594,7 @@ export default function SystemStorageTab() { App {storageHealth.retentionDays.app}d - {storageHealth.tableMaxRows?.callLogs?.toLocaleString() || "100K"} rows + {formatRows(storageHealth.tableMaxRows?.callLogs)} rows diff --git a/src/app/api/usage/analytics/route.ts b/src/app/api/usage/analytics/route.ts index 43609b98ec..cf6ddadcec 100644 --- a/src/app/api/usage/analytics/route.ts +++ b/src/app/api/usage/analytics/route.ts @@ -136,6 +136,20 @@ function resolveModelPricing( } } + // Last resort fallback for historical usage (e.g. "gpt-4" missing, matches "gpt-4.1" or first available) + if (!pricing && providerPricing && typeof providerPricing === "object") { + for (const [key, val] of Object.entries(providerPricing as Record)) { + if (key.includes(lowerModel) || lowerModel.includes(key)) { + pricing = val; + break; + } + } + if (!pricing) { + const keys = Object.keys(providerPricing as Record); + if (keys.length > 0) pricing = (providerPricing as Record)[keys[0]]; + } + } + return pricing as Record | null; } diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 3e394eecf6..989ef7987d 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index bf0f4219a7..bcb9fa6050 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Системен ред", "thinkingBudget": "Мислен бюджет", "proxy": "Прокси", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Ценообразуване", "storage": "Съхранение", "policies": "Политики", diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index 1f515d4731..c7d76a36e7 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index da5434d9f1..1253886043 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Systémový Prompt", "thinkingBudget": "Promýšlení rozpočtu", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Ceny", "storage": "Skladování", "policies": "Zásady", diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 7d72d70e76..5419e633db 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index ea9b7aab13..5daaaa212d 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 7864e5739b..b2f4c692f4 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -3102,6 +3102,22 @@ "systemPrompt": "System Prompt", "thinkingBudget": "Thinking Budget", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "mitmProxy": "MITM Proxy", "pricing": "Pricing", "storage": "Storage", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 4f720e64ca..84ebe91b16 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 3fd00019bd..3a15e48922 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index fbc5ce6b00..af2b333bb2 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Järjestelmäkehote", "thinkingBudget": "Miettivä budjetti", "proxy": "Välityspalvelin", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Hinnoittelu", "storage": "Varastointi", "policies": "Käytännöt", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 2dae9b6bc7..652f2add5e 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index cd892e0a86..1e8450a550 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index 320aace897..a91b77250b 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -2720,6 +2720,22 @@ "systemPrompt": "הנחית מערכת", "thinkingBudget": "חשיבה תקציב", "proxy": "פרוקסי", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "תמחור", "storage": "אחסון", "policies": "מדיניות", diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 45a6adc004..0cb3b55b9e 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -2720,6 +2720,22 @@ "systemPrompt": "सिस्टम प्रॉम्प्ट", "thinkingBudget": "सोच बजट", "proxy": "प्रॉक्सी", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "मूल्य निर्धारण", "storage": "भंडारण", "policies": "नीतियाँ", diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index cc3c5c5d8c..75d4714f9b 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Rendszer Prompt", "thinkingBudget": "Gondolkodó költségvetés", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Árképzés", "storage": "Tárolás", "policies": "Irányelvek", diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index 9f6f223289..a7f0f928a3 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Perintah Sistem", "thinkingBudget": "Memikirkan Anggaran", "proxy": "Proksi", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Harga", "storage": "Penyimpanan", "policies": "Kebijakan", diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index ac67df7b5b..68de27273f 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index 7e4299d56e..d5951e5591 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index c13b1ac971..7d7ab409e3 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -2720,6 +2720,22 @@ "systemPrompt": "システムプロンプト", "thinkingBudget": "予算を考える", "proxy": "プロキシ", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "価格設定", "storage": "ストレージ", "policies": "ポリシー", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 0f3d1a5255..0c92c7fd0e 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -2720,6 +2720,22 @@ "systemPrompt": "시스템 프롬프트", "thinkingBudget": "생각하는 예산", "proxy": "프록시", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "가격", "storage": "저장", "policies": "정책", diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 9912fb1b92..230af1fa18 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index a540423f89..3022f9b12b 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index c58ea7a251..045d8022f0 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Systeemprompt", "thinkingBudget": "Denkbudget", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Prijzen", "storage": "Opslag", "policies": "Beleid", diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index e090186c9c..e63f57edd8 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index c415876fc7..6daab89dad 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -2720,6 +2720,22 @@ "systemPrompt": "System Prompt", "thinkingBudget": "Pag-iisip na Badyet", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Pagpepresyo", "storage": "Imbakan", "policies": "Mga patakaran", diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index f8adc93193..173b306384 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 66ac9b0acf..0c6357e7a5 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -2822,6 +2822,22 @@ "systemPrompt": "Prompt do Sistema", "thinkingBudget": "Orçamento de Raciocínio", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Preços", "storage": "Armazenamento", "policies": "Políticas", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index a19af333f5..5d04d4603e 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -3381,7 +3381,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 053506f331..9b7f8fee84 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Prompt de sistem", "thinkingBudget": "Gândirea bugetului", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Prețuri", "storage": "Depozitare", "policies": "Politici", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index cae73cb853..6061dda7d7 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -2744,6 +2744,22 @@ "systemPrompt": "Системная подсказка", "thinkingBudget": "Думая о бюджете", "proxy": "Прокси", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Цены", "storage": "Хранение", "policies": "Политика", diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 6689ff8953..7355157afa 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Systémová výzva", "thinkingBudget": "Myslenie na rozpočet", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Stanovenie cien", "storage": "Skladovanie", "policies": "zásady", diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index de3fb24ebd..a925c89a80 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Systemprompt", "thinkingBudget": "Tänkande budget", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Prissättning", "storage": "Förvaring", "policies": "Policyer", diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index ac67df7b5b..68de27273f 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index 786feb20cd..d63405fb2a 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index fec11bcd6c..e19a68b3eb 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 246199a50b..9a1535082f 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 10b1e878ec..9a0908adf5 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Sistem İstemi", "thinkingBudget": "Düşünme Bütçesi", "proxy": "Proxy", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Fiyatlandırma", "storage": "Depolama", "policies": "Politikalar", diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index b1f41dbac6..a510185d5c 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -2720,6 +2720,22 @@ "systemPrompt": "Системна підказка", "thinkingBudget": "Мислення про бюджет", "proxy": "Проксі", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "Ціноутворення", "storage": "Зберігання", "policies": "політики", diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 295537cb4c..7a5f38120a 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -3451,7 +3451,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index d351d7ab12..679a8ac539 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -3294,7 +3294,23 @@ "compressionModeRtk": "RTK", "compressionModeRtkDesc": "Command-aware tool output filtering", "compressionModeStacked": "Stacked", - "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression" + "compressionModeStackedDesc": "RTK tool-output filtering followed by Caveman message compression", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}" }, "contextRtk": { "title": "RTK Engine", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index a60a33602e..9cce2c33d9 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -2821,6 +2821,22 @@ "systemPrompt": "系统提示", "thinkingBudget": "思考预算", "proxy": "代理", + "httpProxy": "HTTP Proxy", + "1proxy": "1proxy", + "proxySubTabsAria": "Proxy sections", + "requestBodyLimitTitle": "Request Body Limit", + "requestBodyLimitDescription": "Maximum API payload size accepted before a request body is parsed. Dedicated upload routes keep at least their built-in 100 MB limit.", + "requestBodyLimitInputLabel": "Request body limit in MB", + "requestBodyLimitEmptyError": "Enter a limit in MB", + "requestBodyLimitWholeNumberError": "Use a whole number", + "requestBodyLimitMinimumError": "Minimum is {min} MB", + "requestBodyLimitMaximumError": "Maximum is {max} MB", + "requestBodyLimitLoadFailed": "Failed to load request limit settings", + "requestBodyLimitSaveSuccess": "Request body limit saved", + "requestBodyLimitSaveFailed": "Failed to save request body limit", + "requestBodyLimitSaving": "Saving...", + "requestBodyLimitSave": "Save", + "requestBodyLimitCurrent": "Current: {value}", "pricing": "定价", "storage": "存储", "policies": "策略", diff --git a/src/lib/db/core.ts b/src/lib/db/core.ts index 6e109ebc7f..4f2fc6d5ce 100644 --- a/src/lib/db/core.ts +++ b/src/lib/db/core.ts @@ -16,6 +16,7 @@ import { writeCallArtifact, type CallLogArtifact, } from "../usage/callLogArtifacts"; +import { autoMigrateLegacyEncryptedConnections } from "./providers"; type SqliteDatabase = import("better-sqlite3").Database; type JsonRecord = Record; @@ -1248,6 +1249,15 @@ export function getDbInstance(): SqliteDatabase { } setDb(db); + + // Re-encrypt any tokens using the legacy dynamic salt to canonical static salt + try { + autoMigrateLegacyEncryptedConnections(); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + console.error(`[DB] Legacy encryption migration failed: ${message}`); + } + startDbHealthCheckScheduler(db); console.log(`[DB] SQLite database ready: ${sqliteFile}`); return db; diff --git a/src/lib/db/encryption.ts b/src/lib/db/encryption.ts index 4251768eb5..f1c71f4fd4 100644 --- a/src/lib/db/encryption.ts +++ b/src/lib/db/encryption.ts @@ -34,7 +34,7 @@ const PREFIX = "enc:v1:"; const STATIC_SALT = "omniroute-field-encryption-v1"; let _staticKey: Buffer | null = null; - +let _legacyDynamicKey: Buffer | null = null; /** Connection object with potentially encrypted credential fields. */ export interface ConnectionFields { apiKey?: string | null; @@ -68,6 +68,28 @@ function getStaticKey(): Buffer | null { return _staticKey; } +/** + * Derive the LEGACY key using the old dynamic salt method. + * Used exclusively for fallback decryption of tokens encrypted by older versions. + * + * The old dynamic salt was: createHash("sha256").update(secret).digest().slice(0, 16) + * This produced a different derived key than the static salt, causing incompatibility. + */ +function getLegacyDynamicKey(): Buffer | null { + if (_legacyDynamicKey !== null) return _legacyDynamicKey; + + const secret = process.env.STORAGE_ENCRYPTION_KEY; + if (!secret || typeof secret !== "string" || secret.trim().length === 0) return null; + + const dynamicSalt = createHash("sha256").update(secret).digest().slice(0, 16); + try { + _legacyDynamicKey = scryptSync(secret, dynamicSalt, KEY_LENGTH); + } catch { + return null; + } + return _legacyDynamicKey; +} + /** Check if encryption is enabled. */ export function isEncryptionEnabled(): boolean { return !!process.env.STORAGE_ENCRYPTION_KEY; @@ -243,3 +265,60 @@ export function validateEncryptionConfig(): { valid: boolean; error?: string } { }; } } + +/** + * Specifically tests a ciphertext against the legacy key. If it succeeds, it + * re-encrypts the decrypted value with the canonical static key. + * Used exclusively by the startup migration script. + */ +export function migrateLegacyEncryptedString(ciphertext: string | null | undefined): { + updated: boolean; + value: string | null | undefined; +} { + if (!isEncryptionEnabled()) return { updated: false, value: ciphertext }; + if (!ciphertext || ciphertext.trim().length === 0) return { updated: false, value: ciphertext }; + if (!ciphertext.startsWith(PREFIX)) return { updated: false, value: ciphertext }; + + const staticKey = getStaticKey(); + const legacyKey = getLegacyDynamicKey(); + + if (!staticKey) return { updated: false, value: null }; + + const rawPayload = ciphertext.slice(PREFIX.length); + const parts = rawPayload.split(":"); + if (parts.length !== 3) return { updated: false, value: ciphertext }; + + const [ivHex, authTagHex, encryptedHex] = parts; + const iv = Buffer.from(ivHex, "hex"); + const authTag = Buffer.from(authTagHex, "hex"); + const encrypted = Buffer.from(encryptedHex, "hex"); + + const tryDecryptWithKey = (key: Buffer): string | null => { + try { + const decipher = createDecipheriv(ALGORITHM, key, iv); + decipher.setAuthTag(authTag); + let decrypted = decipher.update(encrypted, undefined, "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; + } catch { + return null; + } + }; + + // 1. If it already decrypts with the static key, no migration needed. + if (tryDecryptWithKey(staticKey) !== null) { + return { updated: false, value: ciphertext }; + } + + // 2. If it decrypts with the legacy key, it needs migration! + if (legacyKey) { + const legacyDecrypted = tryDecryptWithKey(legacyKey); + if (legacyDecrypted !== null) { + // Re-encrypt using the canonical static key and return updated + return { updated: true, value: encrypt(legacyDecrypted) }; + } + } + + // 3. Un-decryptable or corrupted, leave it alone + return { updated: false, value: ciphertext }; +} diff --git a/src/lib/db/providers.ts b/src/lib/db/providers.ts index b5e2c4c03d..fcf6b79fb8 100644 --- a/src/lib/db/providers.ts +++ b/src/lib/db/providers.ts @@ -5,7 +5,11 @@ import { v4 as uuidv4 } from "uuid"; import { getDbInstance, rowToCamel, cleanNulls } from "./core"; import { backupDbFile } from "./backup"; -import { encryptConnectionFields, decryptConnectionFields } from "./encryption"; +import { + encryptConnectionFields, + decryptConnectionFields, + migrateLegacyEncryptedString, +} from "./encryption"; import { invalidateDbCache } from "./readCache"; import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults"; @@ -487,6 +491,67 @@ export async function getDistinctGroups(): Promise { return rows.map((r) => String(r.group ?? "")).filter(Boolean); } +// ──────────────── Auto Migration ──────────────── + +/** + * Scans all connections and re-encrypts any fields using the old dynamic salt + * so they use the new canonical static salt. + */ +export function autoMigrateLegacyEncryptedConnections(): number { + const db = getDbInstance() as unknown as DbLike; + const rows = db.prepare("SELECT * FROM provider_connections").all(); + let migratedCount = 0; + + for (const row of rows) { + const camelRow = rowToCamel(row); + if (!camelRow) continue; + + let updatedRow = false; + + const encryptedFields = ["apiKey", "idToken", "accessToken", "refreshToken"]; + for (const field of encryptedFields) { + if (typeof camelRow[field] === "string") { + const { updated, value } = migrateLegacyEncryptedString(camelRow[field] as string); + if (updated) { + camelRow[field] = value; + updatedRow = true; + } + } + } + + if (updatedRow) { + // camelRow[field] is already re-encrypted! + // But _updateConnectionRow does not re-encrypt automatically, so we pass it safely. + // Wait, _updateConnectionRow runs the full data through `encryptConnectionFields`, + // but `encryptConnectionFields` will re-encrypt plain text. + // BUT `migrateLegacyEncryptedString` returns ALREADY ENCRYPTED ciphertext! + // Wait... if we pass ALREADY ENCRYPTED text to `_updateConnectionRow`, + // `encryptConnectionFields` in `_updateConnectionRow` will encrypt it AGAIN! + // Let's modify the DB directly so we don't double encrypt. + + db.prepare( + "UPDATE provider_connections SET api_key = @apiKey, id_token = @idToken, access_token = @accessToken, refresh_token = @refreshToken, updated_at = @updatedAt WHERE id = @id" + ).run({ + id: camelRow.id, + apiKey: camelRow.apiKey ?? null, + idToken: camelRow.idToken ?? null, + accessToken: camelRow.accessToken ?? null, + refreshToken: camelRow.refreshToken ?? null, + updatedAt: new Date().toISOString(), + }); + migratedCount++; + } + } + + if (migratedCount > 0) { + backupDbFile("pre-write"); + invalidateDbCache("connections"); + console.log(`[DB] Auto-migrated ${migratedCount} connection(s) to new static-salt encryption.`); + } + + return migratedCount; +} + // ──────────────── Provider Nodes ──────────────── export async function getProviderNodes(filter: JsonRecord = {}) { diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index 2ae015e74b..45652194ff 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -8,6 +8,7 @@ import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels. import { invalidateDbCache } from "./readCache"; import { resolveProxyForConnectionFromRegistry } from "./proxies"; import { getComboModelProvider as getComboEntryProvider } from "@/lib/combos/steps"; +import { requestBodyLimitMbFromEnv } from "@/shared/constants/bodySize"; type JsonRecord = Record; type PricingModels = Record; @@ -63,6 +64,7 @@ export async function getSettings() { alwaysPreserveClientCache: "auto", idempotencyWindowMs: 5000, wsAuth: false, + maxBodySizeMb: requestBodyLimitMbFromEnv(process.env.MAX_BODY_SIZE_BYTES), }; for (const row of rows) { const record = toRecord(row); diff --git a/src/lib/usage/migrations.ts b/src/lib/usage/migrations.ts index d44febaf95..cce7648edb 100644 --- a/src/lib/usage/migrations.ts +++ b/src/lib/usage/migrations.ts @@ -281,8 +281,10 @@ export function migrateUsageJsonToSqlite() { connectionId: entry.connectionId || null, apiKeyId: entry.apiKeyId || null, apiKeyName: entry.apiKeyName || null, - tokensInput: entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? 0, - tokensOutput: entry.tokens?.output ?? entry.tokens?.completion_tokens ?? 0, + tokensInput: + entry.tokens?.input ?? entry.tokens?.prompt_tokens ?? entry.tokens?.in ?? 0, + tokensOutput: + entry.tokens?.output ?? entry.tokens?.completion_tokens ?? entry.tokens?.out ?? 0, tokensCacheRead: entry.tokens?.cacheRead ?? entry.tokens?.cached_tokens ?? 0, tokensCacheCreation: entry.tokens?.cacheCreation ?? entry.tokens?.cache_creation_input_tokens ?? 0, diff --git a/src/server/authz/pipeline.ts b/src/server/authz/pipeline.ts index c6d053b809..137d4458c7 100644 --- a/src/server/authz/pipeline.ts +++ b/src/server/authz/pipeline.ts @@ -1,5 +1,6 @@ import { jwtVerify, SignJWT } from "jose"; import { NextResponse, type NextRequest } from "next/server"; +import { getCachedSettings } from "../../lib/db/readCache"; import { isDraining } from "../../lib/gracefulShutdown"; import { checkBodySize, getBodySizeLimit } from "../../shared/middleware/bodySizeGuard"; import { generateRequestId } from "../../shared/utils/requestId"; @@ -168,6 +169,18 @@ function stampRouteResponse( return response; } +async function getBodySizeSettings(): Promise | undefined> { + try { + return await getCachedSettings(); + } catch (error) { + console.warn( + "[Authz] Failed to load request body limit settings:", + error instanceof Error ? error.message : error + ); + return undefined; + } +} + export async function runAuthzPipeline( request: NextRequest, options: AuthzPipelineOptions = {} @@ -194,7 +207,11 @@ export async function runAuthzPipeline( } if (guardedPathname.startsWith("/api/") && method !== "GET" && method !== "OPTIONS") { - const bodySizeRejection = checkBodySize(request, getBodySizeLimit(guardedPathname)); + const bodySizeSettings = await getBodySizeSettings(); + const bodySizeRejection = checkBodySize( + request, + getBodySizeLimit(guardedPathname, bodySizeSettings) + ); if (bodySizeRejection) { stampRouteResponse(bodySizeRejection, requestId, classification.routeClass); applyCorsHeaders(bodySizeRejection, request); diff --git a/src/shared/constants/bodySize.ts b/src/shared/constants/bodySize.ts new file mode 100644 index 0000000000..d623d67898 --- /dev/null +++ b/src/shared/constants/bodySize.ts @@ -0,0 +1,39 @@ +export const REQUEST_BODY_BYTES_PER_MB = 1024 * 1024; +export const DEFAULT_REQUEST_BODY_LIMIT_MB = 10; +export const MIN_REQUEST_BODY_LIMIT_MB = 1; +export const MAX_REQUEST_BODY_LIMIT_MB = 500; +export const DEFAULT_REQUEST_BODY_LIMIT_BYTES = + DEFAULT_REQUEST_BODY_LIMIT_MB * REQUEST_BODY_BYTES_PER_MB; + +export function normalizeRequestBodyLimitMb(value: unknown): number | null { + const parsed = + typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; + if (!Number.isFinite(parsed)) return null; + + const normalized = Math.floor(parsed); + if (normalized < MIN_REQUEST_BODY_LIMIT_MB || normalized > MAX_REQUEST_BODY_LIMIT_MB) { + return null; + } + + return normalized; +} + +export function requestBodyLimitMbToBytes(value: number): number { + return value * REQUEST_BODY_BYTES_PER_MB; +} + +export function parseRequestBodyLimitBytes(value: string | undefined): number { + if (!value) return DEFAULT_REQUEST_BODY_LIMIT_BYTES; + + const parsed = parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_REQUEST_BODY_LIMIT_BYTES; +} + +export function requestBodyLimitBytesToMb(value: number): number { + const configuredMb = Math.round(value / REQUEST_BODY_BYTES_PER_MB); + return Math.min(MAX_REQUEST_BODY_LIMIT_MB, Math.max(MIN_REQUEST_BODY_LIMIT_MB, configuredMb)); +} + +export function requestBodyLimitMbFromEnv(value: string | undefined): number { + return requestBodyLimitBytesToMb(parseRequestBodyLimitBytes(value)); +} diff --git a/src/shared/middleware/bodySizeGuard.ts b/src/shared/middleware/bodySizeGuard.ts index 80c3a4946f..d9ef26fce3 100644 --- a/src/shared/middleware/bodySizeGuard.ts +++ b/src/shared/middleware/bodySizeGuard.ts @@ -13,8 +13,12 @@ * @module shared/middleware/bodySizeGuard */ -/** Default maximum body size: 10 MB */ -const DEFAULT_MAX_BODY_BYTES = 10 * 1024 * 1024; +import { + normalizeRequestBodyLimitMb, + parseRequestBodyLimitBytes, + requestBodyLimitBytesToMb, + requestBodyLimitMbToBytes, +} from "../constants/bodySize"; /** Larger limit for backup/import routes: 100 MB */ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024; @@ -23,10 +27,7 @@ export const MAX_BODY_BYTES_IMPORT = 100 * 1024 * 1024; export const MAX_BODY_BYTES_AUDIO = 100 * 1024 * 1024; /** Configured limit — reads from env or falls back to 10 MB */ -export const MAX_BODY_BYTES = parseInt( - process.env.MAX_BODY_SIZE_BYTES || String(DEFAULT_MAX_BODY_BYTES), - 10 -); +export const MAX_BODY_BYTES = parseRequestBodyLimitBytes(process.env.MAX_BODY_SIZE_BYTES); type BodySizeRule = { prefix: string; limit: number }; @@ -35,12 +36,22 @@ const ROUTE_LIMITS: BodySizeRule[] = [ { prefix: "/api/v1/audio/transcriptions", limit: MAX_BODY_BYTES_AUDIO }, ]; +export function getDefaultRequestBodyLimitMb(): number { + return requestBodyLimitBytesToMb(MAX_BODY_BYTES); +} + +export function getConfiguredBodySizeLimitBytes(settings?: Record): number { + const configuredMb = normalizeRequestBodyLimitMb(settings?.maxBodySizeMb); + return configuredMb === null ? MAX_BODY_BYTES : requestBodyLimitMbToBytes(configuredMb); +} + /** * Resolve the body size limit for a request path. */ -export function getBodySizeLimit(pathname: string): number { +export function getBodySizeLimit(pathname: string, settings?: Record): number { + const configuredLimit = getConfiguredBodySizeLimitBytes(settings); const customRule = ROUTE_LIMITS.find((rule) => pathname.startsWith(rule.prefix)); - return customRule?.limit ?? MAX_BODY_BYTES; + return customRule ? Math.max(customRule.limit, configuredLimit) : configuredLimit; } /** diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index b28f347cb6..97a41d118b 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -4,6 +4,7 @@ import { ROUTING_STRATEGY_VALUES, } from "@/shared/constants/routingStrategies"; import { SUPPORTED_BATCH_ENDPOINTS } from "@/shared/constants/batchEndpoints"; +import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; import { isLocalProvider } from "@/shared/constants/providers"; import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; @@ -444,6 +445,12 @@ export const updateSettingsSchema = z.object({ stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(), requestRetry: z.number().int().min(0).max(10).optional(), maxRetryIntervalSec: z.number().int().min(0).max(300).optional(), + maxBodySizeMb: z + .number() + .int() + .min(MIN_REQUEST_BODY_LIMIT_MB) + .max(MAX_REQUEST_BODY_LIMIT_MB) + .optional(), // Auto intent classifier settings (multilingual routing) intentDetectionEnabled: z.boolean().optional(), intentSimpleMaxWords: z.number().int().min(1).max(500).optional(), diff --git a/src/shared/validation/settingsSchemas.ts b/src/shared/validation/settingsSchemas.ts index a11aaee1c9..ec6b67d8e7 100644 --- a/src/shared/validation/settingsSchemas.ts +++ b/src/shared/validation/settingsSchemas.ts @@ -1,4 +1,109 @@ -// ... existing imports ... +/** + * Settings-specific Zod schemas. + * + * Extracted from schemas.ts to work around the webpack barrel-file + * optimization bug that makes large schema barrel exports `undefined` + * at runtime (see: https://github.com/vercel/next.js/issues/12557). + */ +import { z } from "zod"; +import { COMBO_CONFIG_MODES } from "@/shared/constants/comboConfigMode"; +import { MAX_REQUEST_BODY_LIMIT_MB, MIN_REQUEST_BODY_LIMIT_MB } from "@/shared/constants/bodySize"; +import { HIDEABLE_SIDEBAR_ITEM_IDS } from "@/shared/constants/sidebarVisibility"; +import { ACCOUNT_FALLBACK_STRATEGY_VALUES } from "@/shared/constants/routingStrategies"; + +const signatureCacheModeValues = ["enabled", "bypass", "bypass-strict"] as const; + +export const updateSettingsSchema = z.object({ + newPassword: z.string().min(1).max(200).optional(), + currentPassword: z.string().max(200).optional(), + theme: z.string().max(50).optional(), + language: z.string().max(10).optional(), + requireLogin: z.boolean().optional(), + enableSocks5Proxy: z.boolean().optional(), + instanceName: z.string().max(100).optional(), + customLogoUrl: z.string().max(2000).optional(), + customLogoBase64: z.string().max(100000).optional(), + customFaviconUrl: z.string().max(2000).optional(), + customFaviconBase64: z.string().max(50000).optional(), + corsOrigins: z.string().max(500).optional(), + cloudUrl: z.string().max(500).optional(), + baseUrl: z.string().max(500).optional(), + setupComplete: z.boolean().optional(), + blockedProviders: z.array(z.string().max(100)).optional(), + hideHealthCheckLogs: z.boolean().optional(), + hideEndpointCloudflaredTunnel: z.boolean().optional(), + hideEndpointTailscaleFunnel: z.boolean().optional(), + hideEndpointNgrokTunnel: z.boolean().optional(), + debugMode: z.boolean().optional(), + hiddenSidebarItems: z.array(z.enum(HIDEABLE_SIDEBAR_ITEM_IDS)).optional(), + comboConfigMode: z.enum(COMBO_CONFIG_MODES).optional(), + // Routing settings (#134) + fallbackStrategy: z.enum(ACCOUNT_FALLBACK_STRATEGY_VALUES).optional(), + wildcardAliases: z.array(z.object({ pattern: z.string(), target: z.string() })).optional(), + stickyRoundRobinLimit: z.number().int().min(0).max(1000).optional(), + requestRetry: z.number().int().min(0).max(10).optional(), + maxRetryIntervalSec: z.number().int().min(0).max(300).optional(), + maxBodySizeMb: z + .number() + .int() + .min(MIN_REQUEST_BODY_LIMIT_MB) + .max(MAX_REQUEST_BODY_LIMIT_MB) + .optional(), + // Auto intent classifier settings (multilingual routing) + intentDetectionEnabled: z.boolean().optional(), + intentSimpleMaxWords: z.number().int().min(1).max(500).optional(), + intentExtraCodeKeywords: z.array(z.string().max(100)).optional(), + intentExtraReasoningKeywords: z.array(z.string().max(100)).optional(), + intentExtraSimpleKeywords: z.array(z.string().max(100)).optional(), + // Protocol toggles (default: disabled) + mcpEnabled: z.boolean().optional(), + mcpTransport: z.enum(["stdio", "sse", "streamable-http"]).optional(), + a2aEnabled: z.boolean().optional(), + wsAuth: z.boolean().optional(), + // CLI Fingerprint compatibility (per-provider) + cliCompatProviders: z.array(z.string().max(100)).optional(), + // Strip provider/model prefix at proxy layer (e.g. "openai/gpt-4" → "gpt-4") + stripModelPrefix: z.boolean().optional(), + // Cache control preservation mode + alwaysPreserveClientCache: z.enum(["auto", "always", "never"]).optional(), + antigravitySignatureCacheMode: z.enum(signatureCacheModeValues).optional(), + // Adaptive Volume Routing + adaptiveVolumeRouting: z.boolean().optional(), + // Usage token buffer — safety margin added to reported prompt/input token counts. + // Prevents CLI tools from overrunning context windows. Set to 0 to disable. + usageTokenBuffer: z.number().int().min(0).max(50000).optional(), + // Custom CLI agent definitions for ACP + customAgents: z + .array( + z.object({ + id: z.string().max(50), + name: z.string().max(100), + binary: z.string().max(200), + versionCommand: z.string().max(300), + providerAlias: z.string().max(50), + spawnArgs: z.array(z.string().max(200)), + protocol: z.enum(["stdio", "http"]), + }) + ) + .optional(), + // SkillsMP marketplace API key + skillsmpApiKey: z.string().max(200).optional(), + // Active skills provider (single source of truth for skills page) + skillsProvider: z.enum(["skillsmp", "skillssh"]).optional(), + // models.dev sync settings + modelsDevSyncEnabled: z.boolean().optional(), + modelsDevSyncInterval: z.number().int().min(3600000).max(604800000).optional(), + // Vision Bridge settings + visionBridgeEnabled: z.boolean().optional(), + visionBridgeModel: z.string().max(200).optional(), + visionBridgePrompt: z.string().max(5000).optional(), + visionBridgeTimeout: z.number().int().min(1000).max(300000).optional(), + visionBridgeMaxImages: z.number().int().min(1).max(20).optional(), + // Missing settings + lkgpEnabled: z.boolean().optional(), + backgroundDegradation: z.unknown().optional(), + bruteForceProtection: z.boolean().optional(), +}); export const databaseSettingsSchema = z.object( { @@ -69,5 +174,3 @@ export const databaseSettingsSchema = z.object( ); export type DatabaseSettingsSchema = z.infer; - -// ... rest of the file ... diff --git a/src/types/settings.ts b/src/types/settings.ts index e8143246b2..3a4c5ad202 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -15,6 +15,7 @@ export interface Settings { stickyRoundRobinLimit: number; requestRetry: number; maxRetryIntervalSec: number; + maxBodySizeMb?: number; jwtSecret?: string; mcpEnabled?: boolean; mcpTransport?: "stdio" | "sse" | "streamable-http"; diff --git a/tests/unit/body-size-guard.test.ts b/tests/unit/body-size-guard.test.ts new file mode 100644 index 0000000000..2f5b967ea2 --- /dev/null +++ b/tests/unit/body-size-guard.test.ts @@ -0,0 +1,42 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + MAX_BODY_BYTES_AUDIO, + getBodySizeLimit, + checkBodySize, +} from "../../src/shared/middleware/bodySizeGuard.ts"; +import { requestBodyLimitMbToBytes } from "../../src/shared/constants/bodySize.ts"; + +test("body size guard uses maxBodySizeMb from settings for regular API routes", () => { + assert.equal( + getBodySizeLimit("/api/v1/responses", { maxBodySizeMb: 100 }), + requestBodyLimitMbToBytes(100) + ); +}); + +test("body size guard keeps dedicated upload limits as lower bounds", () => { + assert.equal( + getBodySizeLimit("/api/v1/audio/transcriptions", { maxBodySizeMb: 1 }), + MAX_BODY_BYTES_AUDIO + ); + assert.equal( + getBodySizeLimit("/api/v1/audio/transcriptions", { maxBodySizeMb: 200 }), + requestBodyLimitMbToBytes(200) + ); +}); + +test("checkBodySize reports the configured request limit in 413 responses", async () => { + const limit = requestBodyLimitMbToBytes(100); + const request = new Request("http://localhost/api/v1/responses", { + method: "POST", + headers: { "content-length": String(limit + 1) }, + }); + + const response = checkBodySize(request, limit); + + assert.ok(response); + assert.equal(response.status, 413); + const body = await response.json(); + assert.equal(body.error.code, "PAYLOAD_TOO_LARGE"); + assert.match(body.error.message, /100 MB/); +}); diff --git a/tests/unit/settings-i18n-keys.test.ts b/tests/unit/settings-i18n-keys.test.ts index 56efc1098e..f111c62124 100644 --- a/tests/unit/settings-i18n-keys.test.ts +++ b/tests/unit/settings-i18n-keys.test.ts @@ -1,10 +1,13 @@ import test from "node:test"; import assert from "node:assert/strict"; import { createRequire } from "node:module"; +import fs from "node:fs"; +import path from "node:path"; const require = createRequire(import.meta.url); const en = require("../../src/i18n/messages/en.json"); const zhCn = require("../../src/i18n/messages/zh-CN.json"); +const { SIDEBAR_SECTIONS } = await import("../../src/shared/constants/sidebarVisibility.ts"); const requiredSettingsKeys = [ "adaptiveVolumeRouting", @@ -21,9 +24,85 @@ const requiredSettingsKeys = [ "purgeLogsFailed", ]; +const requestBodyLimitSettingsKeys = [ + "requestBodyLimitTitle", + "requestBodyLimitDescription", + "requestBodyLimitInputLabel", + "requestBodyLimitEmptyError", + "requestBodyLimitWholeNumberError", + "requestBodyLimitMinimumError", + "requestBodyLimitMaximumError", + "requestBodyLimitLoadFailed", + "requestBodyLimitSaveSuccess", + "requestBodyLimitSaveFailed", + "requestBodyLimitSaving", + "requestBodyLimitSave", + "requestBodyLimitCurrent", +]; + +const proxyPageSettingsKeys = ["httpProxy", "1proxy", "proxySubTabsAria"]; + test("settings translations include LKGP and maintenance keys in English and Simplified Chinese", () => { for (const key of requiredSettingsKeys) { assert.equal(typeof en.settings?.[key], "string", `en.settings.${key} should exist`); assert.equal(typeof zhCn.settings?.[key], "string", `zh-CN.settings.${key} should exist`); } }); + +test("English sidebar translations include every configured sidebar item", () => { + const sidebarKeys = new Set( + SIDEBAR_SECTIONS.flatMap((section) => [ + section.titleKey, + ...section.items.map((item) => item.i18nKey), + ]) + ); + + for (const key of sidebarKeys) { + assert.equal(typeof en.sidebar?.[key], "string", `en.sidebar.${key} should exist`); + } +}); + +test("all locales include the proxy sidebar label", () => { + const messagesDir = path.resolve(process.cwd(), "src/i18n/messages"); + const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json")); + + for (const file of messageFiles) { + const messages = require(path.join(messagesDir, file)); + + assert.equal(typeof messages.sidebar?.proxy, "string", `${file}: sidebar.proxy should exist`); + } +}); + +test("all locales include request body limit settings labels", () => { + const messagesDir = path.resolve(process.cwd(), "src/i18n/messages"); + const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json")); + + for (const file of messageFiles) { + const messages = require(path.join(messagesDir, file)); + + for (const key of requestBodyLimitSettingsKeys) { + assert.equal( + typeof messages.settings?.[key], + "string", + `${file}: settings.${key} should exist` + ); + } + } +}); + +test("all locales include proxy page tab labels", () => { + const messagesDir = path.resolve(process.cwd(), "src/i18n/messages"); + const messageFiles = fs.readdirSync(messagesDir).filter((file) => file.endsWith(".json")); + + for (const file of messageFiles) { + const messages = require(path.join(messagesDir, file)); + + for (const key of proxyPageSettingsKeys) { + assert.equal( + typeof messages.settings?.[key], + "string", + `${file}: settings.${key} should exist` + ); + } + } +}); diff --git a/tests/unit/settings-schema-routing-strategies.test.ts b/tests/unit/settings-schema-routing-strategies.test.ts index bb971d64cd..793ab40e59 100644 --- a/tests/unit/settings-schema-routing-strategies.test.ts +++ b/tests/unit/settings-schema-routing-strategies.test.ts @@ -38,6 +38,18 @@ test("settings schemas accept cooldown-aware retry knobs", () => { assert.equal(sharedParsed.maxRetryIntervalSec, 30); }); +test("settings schemas accept request body limit", () => { + const routeParsed = settingsRouteSchema.parse({ maxBodySizeMb: 100 }); + const sharedParsed = sharedSettingsSchema.parse({ maxBodySizeMb: 100 }); + + assert.equal(routeParsed.maxBodySizeMb, 100); + assert.equal(sharedParsed.maxBodySizeMb, 100); + assert.equal(settingsRouteSchema.safeParse({ maxBodySizeMb: 0 }).success, false); + assert.equal(settingsRouteSchema.safeParse({ maxBodySizeMb: 501 }).success, false); + assert.equal(sharedSettingsSchema.safeParse({ maxBodySizeMb: 0 }).success, false); + assert.equal(sharedSettingsSchema.safeParse({ maxBodySizeMb: 501 }).success, false); +}); + test("settings schemas accept wsAuth toggle", () => { const routeParsed = settingsRouteSchema.parse({ wsAuth: true }); const sharedParsed = sharedSettingsSchema.parse({ wsAuth: false });