From dab9d8972b2452ee466c23f78176b8d4684bc113 Mon Sep 17 00:00:00 2001 From: Spencer Francisco Date: Sun, 2 Nov 2025 08:23:28 -0600 Subject: [PATCH] fix: Improve API key validation to prevent GitHub PAT acceptance - Update API key validation to require 'sk-or-v1-' prefix instead of accepting 'sk-or-' or 'sk-' - Add proper format checking in useSimpleApiKeyStorage hook - Improve error messages for invalid keys - Add validation in ModelSelector before fetching models - Update placeholder text to reflect correct key format Fixes #23 --- components/ApiKeyInput.tsx | 44 +++++++++++++++++++++++++-------- components/ModelSelector.tsx | 26 ++++++++++++++++--- hooks/useSimpleApiKeyStorage.ts | 5 ++-- package-lock.json | 37 ++++++++++++++++++--------- 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/components/ApiKeyInput.tsx b/components/ApiKeyInput.tsx index 2579abf..6ff9091 100644 --- a/components/ApiKeyInput.tsx +++ b/components/ApiKeyInput.tsx @@ -49,6 +49,15 @@ export function ApiKeyInput({ return false } + // Validate key format - OpenRouter keys must start with "sk-or-v1-" + if (!keyToTest.startsWith('sk-or-v1-')) { + setTestResult({ + isValid: false, + error: 'Invalid API key format. OpenRouter keys must start with "sk-or-v1-"' + }) + return false + } + setIsTestingKey(true) setTestResult(null) onLoadingChange?.(true) @@ -67,7 +76,7 @@ export function ApiKeyInput({ } else { setTestResult({ isValid: false, - error: 'Invalid API key' + error: 'Invalid API key - could not connect to OpenRouter. Please check your API key.' }) // Clear invalid key from sessionStorage clearAPIKey() @@ -75,11 +84,26 @@ export function ApiKeyInput({ return false } } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to test API key' + let errorMessage = 'Failed to test API key' + + if (error instanceof Error) { + if (error.message.includes('401') || error.message.includes('403')) { + errorMessage = 'Invalid API key - authentication failed. Please check your OpenRouter API key.' + } else if (error.message.includes('429')) { + errorMessage = 'Rate limit exceeded. Please wait before trying again.' + } else if (error.message.includes('Network') || error.message.includes('fetch')) { + errorMessage = 'Network error - could not connect to OpenRouter. Please check your internet connection.' + } else { + errorMessage = `Validation failed: ${error.message}` + } + } + setTestResult({ isValid: false, - error: `Validation failed: ${errorMessage}` + error: errorMessage }) + // Clear invalid key from sessionStorage + clearAPIKey() onApiKeyValidated?.(false) return false } finally { @@ -112,11 +136,11 @@ export function ApiKeyInput({ return } - // Validate key format - if (!trimmedValue.startsWith('sk-or-') && !trimmedValue.startsWith('sk-')) { + // Validate key format - OpenRouter keys must start with "sk-or-v1-" + if (!trimmedValue.startsWith('sk-or-v1-')) { setTestResult({ isValid: false, - error: 'OpenRouter API keys should start with "sk-or-" or "sk-"' + error: 'OpenRouter API keys must start with "sk-or-v1-"' }) return } @@ -146,11 +170,11 @@ export function ApiKeyInput({ return } - // Validate key format - if (!keyToTest.startsWith('sk-or-') && !keyToTest.startsWith('sk-')) { + // Validate key format - OpenRouter keys must start with "sk-or-v1-" + if (!keyToTest.startsWith('sk-or-v1-')) { setTestResult({ isValid: false, - error: 'Invalid API key format' + error: 'Invalid API key format. OpenRouter keys must start with "sk-or-v1-"' }) onApiKeyValidated?.(false, keyToTest) return @@ -216,7 +240,7 @@ export function ApiKeyInput({
([]) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) @@ -66,6 +66,13 @@ export function ModelSelector({ return } + // Check if API key has valid format + if (!isApiKeyValidFormat) { + setError('Invalid API key format. OpenRouter API keys must start with "sk-or-v1-"') + onError?.() + return + } + setIsLoading(true) setError(null) onLoadingChange?.(true) @@ -81,14 +88,27 @@ export function ModelSelector({ setModels(fetchedModels) } } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Failed to load models' + let errorMessage = 'Failed to load models' + + if (err instanceof Error) { + if (err.message.includes('401') || err.message.includes('403')) { + errorMessage = 'Invalid API key - authentication failed. Please check your OpenRouter API key.' + } else if (err.message.includes('429')) { + errorMessage = 'Rate limit exceeded. Please wait before trying again.' + } else if (err.message.includes('Network') || err.message.includes('fetch')) { + errorMessage = 'Network error - could not connect to OpenRouter. Please check your internet connection.' + } else { + errorMessage = err.message + } + } + setError(errorMessage) onError?.() } finally { setIsLoading(false) onLoadingChange?.(false) } - }, [apiKey]) + }, [apiKey, isApiKeyValidFormat, onError, onLoadingChange]) // Load models when component mounts or API key changes useEffect(() => { diff --git a/hooks/useSimpleApiKeyStorage.ts b/hooks/useSimpleApiKeyStorage.ts index 64fa9f7..ca409ce 100644 --- a/hooks/useSimpleApiKeyStorage.ts +++ b/hooks/useSimpleApiKeyStorage.ts @@ -77,13 +77,14 @@ export function useSimpleApiKeyStorage() { }, []) // Simple validation check - const isValidFormat = apiKey ? apiKey.startsWith('sk-or-') || apiKey.startsWith('sk-') : false + const isValidFormat = apiKey ? apiKey.startsWith('sk-or-v1-') : false const hasValidKey = Boolean(apiKey && isValidFormat && isValidated) return { value: apiKey, hasValidKey, setAPIKey, - clearAPIKey + clearAPIKey, + isValidFormat } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d99175d..8f31c53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openspec-temp", - "version": "0.1.0", + "version": "0.5.5-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openspec-temp", - "version": "0.1.0", + "version": "0.5.5-beta", "dependencies": { "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -144,6 +144,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -794,6 +795,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -817,6 +819,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2943,7 +2946,6 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3033,8 +3035,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3508,6 +3509,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3519,6 +3521,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3623,6 +3626,7 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -4128,6 +4132,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4674,6 +4679,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.2", "caniuse-lite": "^1.0.30001741", @@ -4897,6 +4903,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -5223,6 +5230,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -5632,6 +5640,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -5989,8 +5998,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.2.6", @@ -6278,6 +6286,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6447,6 +6456,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9353,6 +9363,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -9722,7 +9733,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11502,6 +11512,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11623,7 +11634,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11639,7 +11649,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -11652,8 +11661,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/prismjs": { "version": "1.30.0", @@ -11760,6 +11768,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -11772,6 +11781,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13166,6 +13176,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13354,6 +13365,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13613,6 +13625,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"