diff --git a/src/pages/AiProvidersClaudeEditLayout.tsx b/src/pages/AiProvidersClaudeEditLayout.tsx index 7025e0e..2701bb4 100644 --- a/src/pages/AiProvidersClaudeEditLayout.tsx +++ b/src/pages/AiProvidersClaudeEditLayout.tsx @@ -13,6 +13,7 @@ import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } f import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; import { modelsToEntries } from '@/components/ui/modelInputListUtils'; import type { ClaudeEditBaseline } from '@/stores/useClaudeEditDraftStore'; +import { parseRouteIndexParam } from '@/utils/routeParams'; type LocationState = { fromAiProviders?: boolean } | null; @@ -53,12 +54,6 @@ const buildEmptyForm = (): ProviderFormState => ({ excludedText: '', }); -const parseIndexParam = (value: string | undefined) => { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -}; - const getErrorMessage = (err: unknown) => { if (err instanceof Error) return err.message; if (typeof err === 'string') return err; @@ -122,7 +117,7 @@ export function AiProvidersClaudeEditLayout() { const params = useParams<{ index?: string }>(); const hasIndexParam = typeof params.index === 'string'; - const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); + const editIndex = useMemo(() => parseRouteIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const connectionStatus = useAuthStore((state) => state.connectionStatus); diff --git a/src/pages/AiProvidersCodexEditPage.tsx b/src/pages/AiProvidersCodexEditPage.tsx index 98a37d8..b1abe1e 100644 --- a/src/pages/AiProvidersCodexEditPage.tsx +++ b/src/pages/AiProvidersCodexEditPage.tsx @@ -17,6 +17,7 @@ import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import type { ProviderKeyConfig } from '@/types'; import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers'; import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare'; +import { parseRouteIndexParam } from '@/utils/routeParams'; import { entriesToModels, modelsToEntries } from '@/components/ui/modelInputListUtils'; import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; import type { ProviderFormState } from '@/components/providers'; @@ -40,12 +41,6 @@ const buildEmptyForm = (): ProviderFormState => ({ excludedText: '', }); -const parseIndexParam = (value: string | undefined) => { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -}; - const getErrorMessage = (err: unknown) => { if (err instanceof Error) return err.message; if (typeof err === 'string') return err; @@ -121,7 +116,7 @@ export function AiProvidersCodexEditPage() { const modelDiscoveryRequestIdRef = useRef(0); const hasIndexParam = typeof params.index === 'string'; - const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); + const editIndex = useMemo(() => parseRouteIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const initialData = useMemo(() => { diff --git a/src/pages/AiProvidersGeminiEditPage.tsx b/src/pages/AiProvidersGeminiEditPage.tsx index 8dd3ce3..2ca6b15 100644 --- a/src/pages/AiProvidersGeminiEditPage.tsx +++ b/src/pages/AiProvidersGeminiEditPage.tsx @@ -16,6 +16,7 @@ import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import type { GeminiKeyConfig } from '@/types'; import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers'; import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare'; +import { parseRouteIndexParam } from '@/utils/routeParams'; import type { ModelInfo } from '@/utils/models'; import { entriesToModels, modelsToEntries } from '@/components/ui/modelInputListUtils'; import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; @@ -37,12 +38,6 @@ const buildEmptyForm = (): GeminiFormState => ({ excludedText: '', }); -const parseIndexParam = (value: string | undefined) => { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -}; - const stripGeminiModelResourceName = (value: string) => { return String(value ?? '') .trim() @@ -116,7 +111,7 @@ export function AiProvidersGeminiEditPage() { const modelDiscoveryRequestIdRef = useRef(0); const hasIndexParam = typeof params.index === 'string'; - const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); + const editIndex = useMemo(() => parseRouteIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const initialData = useMemo(() => { diff --git a/src/pages/AiProvidersOpenAIEditLayout.tsx b/src/pages/AiProvidersOpenAIEditLayout.tsx index 6ef9997..8abef23 100644 --- a/src/pages/AiProvidersOpenAIEditLayout.tsx +++ b/src/pages/AiProvidersOpenAIEditLayout.tsx @@ -10,6 +10,7 @@ import type { ApiKeyEntry, OpenAIProviderConfig } from '@/types'; import type { ModelInfo } from '@/utils/models'; import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers'; import { areKeyValueEntriesEqual, areModelEntriesEqual } from '@/utils/compare'; +import { parseRouteIndexParam } from '@/utils/routeParams'; import { buildApiKeyEntry } from '@/components/providers/utils'; import type { ModelEntry, OpenAIFormState } from '@/components/providers/types'; import type { KeyTestStatus, OpenAIEditBaseline } from '@/stores/useOpenAIEditDraftStore'; @@ -52,12 +53,6 @@ const buildEmptyForm = (): OpenAIFormState => ({ testModel: undefined, }); -const parseIndexParam = (value: string | undefined) => { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -}; - const getErrorMessage = (err: unknown) => { if (err instanceof Error) return err.message; if (typeof err === 'string') return err; @@ -140,7 +135,7 @@ export function AiProvidersOpenAIEditLayout() { const params = useParams<{ index?: string }>(); const hasIndexParam = typeof params.index === 'string'; - const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); + const editIndex = useMemo(() => parseRouteIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const connectionStatus = useAuthStore((state) => state.connectionStatus); diff --git a/src/pages/AiProvidersVertexEditPage.tsx b/src/pages/AiProvidersVertexEditPage.tsx index be0c3c1..8ad87e8 100644 --- a/src/pages/AiProvidersVertexEditPage.tsx +++ b/src/pages/AiProvidersVertexEditPage.tsx @@ -16,6 +16,7 @@ import type { ProviderKeyConfig } from '@/types'; import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; import { buildHeaderObject, headersToEntries, normalizeHeaderEntries } from '@/utils/headers'; import { areKeyValueEntriesEqual, areModelEntriesEqual, areStringArraysEqual } from '@/utils/compare'; +import { parseRouteIndexParam } from '@/utils/routeParams'; import type { VertexFormState } from '@/components/providers'; import layoutStyles from './AiProvidersEditLayout.module.scss'; @@ -33,12 +34,6 @@ const buildEmptyForm = (): VertexFormState => ({ excludedText: '', }); -const parseIndexParam = (value: string | undefined) => { - if (!value) return null; - const parsed = Number.parseInt(value, 10); - return Number.isFinite(parsed) ? parsed : null; -}; - const normalizeModelEntries = (entries: Array<{ name: string; alias: string }>) => (entries ?? []).reduce>((acc, entry) => { const name = String(entry?.name ?? '').trim(); @@ -93,7 +88,7 @@ export function AiProvidersVertexEditPage() { const [baseline, setBaseline] = useState(() => buildVertexBaseline(buildEmptyForm())); const hasIndexParam = typeof params.index === 'string'; - const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); + const editIndex = useMemo(() => parseRouteIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const initialData = useMemo(() => { diff --git a/src/utils/routeParams.ts b/src/utils/routeParams.ts new file mode 100644 index 0000000..4131a90 --- /dev/null +++ b/src/utils/routeParams.ts @@ -0,0 +1,10 @@ +const ROUTE_INDEX_PATTERN = /^(0|[1-9]\d*)$/; + +export function parseRouteIndexParam(value: string | undefined): number | null { + // Route indexes must be canonical digits only; parseInt would accept values like "1abc" + // and could route a malformed URL to a real provider entry. + if (!value || !ROUTE_INDEX_PATTERN.test(value)) return null; + + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; +}