diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bab9ca..9c8dc43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Generalized Custom Provider endpoint handling and host permissions for OpenAI-compatible providers, including DeepSeek. + ## [1.2.4] - 2026-04-11 ### Added diff --git a/apps/extension/public/manifest.json b/apps/extension/public/manifest.json index a5f2503..c0ad934 100644 --- a/apps/extension/public/manifest.json +++ b/apps/extension/public/manifest.json @@ -41,6 +41,10 @@ "storage", "identity" ], + "optional_host_permissions": [ + "https://*/*", + "http://*/*" + ], "host_permissions": [ "https://api.openai.com/*", "https://generativelanguage.googleapis.com/*", @@ -67,6 +71,6 @@ ] }, "content_security_policy": { - "extension_pages": "script-src 'self'; object-src 'self'; connect-src https://api.openai.com https://generativelanguage.googleapis.com https://www.googleapis.com https://apis.google.com https://www.gstatic.com https://securetoken.googleapis.com https://identitytoolkit.googleapis.com https://firebaseinstallations.googleapis.com https://firestore.googleapis.com https://api.anthropic.com https://api.mistral.ai https://api.groq.com https://openrouter.ai http://localhost:* http://127.0.0.1:*" + "extension_pages": "script-src 'self'; object-src 'self';" } } diff --git a/apps/extension/src/components/ModelConfig.tsx b/apps/extension/src/components/ModelConfig.tsx index 30d4507..cb039ba 100644 --- a/apps/extension/src/components/ModelConfig.tsx +++ b/apps/extension/src/components/ModelConfig.tsx @@ -103,6 +103,24 @@ function getResponseError(value: unknown) { return undefined; } +function getCustomProviderOriginPattern(baseUrl: string) { + const trimmed = baseUrl.trim(); + if (!trimmed) { + return ""; + } + + try { + const url = new URL(trimmed); + if (url.protocol !== "http:" && url.protocol !== "https:") { + return null; + } + + return `${url.origin}/*`; + } catch { + return null; + } +} + function getSyncStatusMeta( status: SyncStatus, storageBackend: StorageBackendPreference, @@ -185,7 +203,9 @@ export function ModelConfig() { selectedProvider === "custom" || selectedProvider === "openrouter"; const loadAuthStatus = async () => { - const response = await chrome.runtime.sendMessage({ type: "GET_AUTH_STATUS" }); + const response = await chrome.runtime.sendMessage({ + type: "GET_AUTH_STATUS", + }); setIsSignedIn(isUserInfoResponse(response)); }; @@ -193,13 +213,16 @@ export function ModelConfig() { const rawResponse = await chrome.runtime.sendMessage({ type: "GET_STORAGE_SETTINGS", }); - const response = isStorageSettingsResponse(rawResponse) ? rawResponse : null; + const response = isStorageSettingsResponse(rawResponse) + ? rawResponse + : null; if (!response) { setBackendState({ loading: false, tone: "error", - message: getResponseError(rawResponse) || "Failed to load sync settings.", + message: + getResponseError(rawResponse) || "Failed to load sync settings.", }); return; } @@ -248,10 +271,7 @@ export function ModelConfig() { window.addEventListener("plenz-auth-status-changed", handleAuthChange); return () => { - window.removeEventListener( - "plenz-auth-status-changed", - handleAuthChange, - ); + window.removeEventListener("plenz-auth-status-changed", handleAuthChange); }; }, []); @@ -261,10 +281,15 @@ export function ModelConfig() { useEffect(() => { const fetchModelsWithCache = async () => { - const p = providers.find((prov) => prov.id === selectedProvider) || providers[0]; + const p = + providers.find((prov) => prov.id === selectedProvider) || providers[0]; const cacheKey = `models_${selectedProvider}`; - - if (!apiKey && selectedProvider !== "openrouter" && selectedProvider !== "custom") { + + if ( + !apiKey && + selectedProvider !== "openrouter" && + selectedProvider !== "custom" + ) { chrome.storage.local.get([cacheKey], (res) => { setModels((res[cacheKey] as ModelOption[]) || []); }); @@ -298,6 +323,41 @@ export function ModelConfig() { const handleTest = async () => { setTestStatus({ loading: true }); try { + if (selectedProvider === "custom") { + const originPattern = getCustomProviderOriginPattern(baseUrl); + + if (!originPattern) { + setTestStatus({ + loading: false, + result: { + success: false, + latencyMs: 0, + error: "Enter a valid http(s) Custom Provider URL.", + }, + }); + return; + } + + if (originPattern) { + const granted = await chrome.permissions.request({ + origins: [originPattern], + }); + + if (!granted) { + setTestStatus({ + loading: false, + result: { + success: false, + latencyMs: 0, + error: + "Permission denied for that provider host. Approve access and try again.", + }, + }); + return; + } + } + } + const response = await chrome.runtime.sendMessage({ type: "TEST_CONNECTION", payload: { @@ -412,6 +472,31 @@ export function ModelConfig() { setSaveState({ status: "syncing" }); try { + if (selectedProvider === "custom") { + const originPattern = getCustomProviderOriginPattern(baseUrl); + + if (!originPattern) { + const errorMessage = "Enter a valid http(s) Custom Provider URL."; + setSaveState({ status: "error", error: errorMessage }); + alert(`Error saving settings: ${errorMessage}`); + return; + } + + if (originPattern) { + const granted = await chrome.permissions.request({ + origins: [originPattern], + }); + + if (!granted) { + const errorMessage = + "Permission denied for that provider host. Approve access before saving."; + setSaveState({ status: "error", error: errorMessage }); + alert(`Error saving settings: ${errorMessage}`); + return; + } + } + } + const rawResponse = await chrome.runtime.sendMessage({ type: "SAVE_MODEL_CONFIG", payload: { @@ -455,20 +540,20 @@ export function ModelConfig() { }; const syncMeta = getSyncStatusMeta(saveState.status, storageBackend); - const defaultBackendMessage = - !firebaseConfigured - ? "Cloud Sync is unavailable in this build. Add the VITE_FIREBASE_* environment variables and rebuild the extension." - : !isSignedIn - ? "Sign in with Google to enable Cloud Sync and access encrypted synced API keys." - : storageBackend === "firebase" - ? "Cloud Sync stores provider settings in Firebase, with API keys encrypted before upload. Browser-local copies are unchanged." - : "Chrome Sync uses Chrome's built-in sync storage and falls back to local storage when needed."; + const defaultBackendMessage = !firebaseConfigured + ? "Cloud Sync is unavailable in this build. Add the VITE_FIREBASE_* environment variables and rebuild the extension." + : !isSignedIn + ? "Sign in with Google to enable Cloud Sync and access encrypted synced API keys." + : storageBackend === "firebase" + ? "Cloud Sync stores provider settings in Firebase, with API keys encrypted before upload. Browser-local copies are unchanged." + : "Chrome Sync uses Chrome's built-in sync storage and falls back to local storage when needed."; return (
+ Enter the exact model ID from your provider. Examples include{" "}
+ deepseek-chat, deepseek-reasoner, or{" "}
+ llama3.1.
+