diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 159b2bc..44521f2 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -36,7 +36,7 @@ A lightweight, browser‑based DAW with an AI Agent "K.G.Studio Musician Assista - K.G.Studio is fully client‑side. It connects to the network only to: - Download instrument soundfonts from the configured CDN - Communicate with your chosen LLM provider (OpenAI or OpenAI‑compatible) -- API keys are not persisted when running from a non‑local host (to reduce XSS risk). You’ll be prompted to re‑enter them on each start in that scenario. +- API keys are not persisted when running from a non‑local host (to reduce XSS risk). You'll be prompted to re‑enter them on each start in that scenario. To opt‑in to persistence on non‑local hosts, enable "Persist API Keys on Non-Localhost" in Settings > General (not recommended for shared/production environments). - Important: While K.G.Studio does not collect your data, different LLM providers may have different data‑retention policies. Review the policy of the provider you select before use. ## 5. User Interface Tour @@ -125,10 +125,10 @@ A lightweight, browser‑based DAW with an AI Agent "K.G.Studio Musician Assista - If loading stalls, the overlay will time out; refresh to retry downloading. ## 13. Settings -- General: LLM provider (OpenAI or OpenAI‑compatible), API key, model, soundfont base URL. +- General: LLM provider (OpenAI or OpenAI‑compatible), API key, model, soundfont base URL, Persist API Keys on Non-Localhost. - Behavior: chatbox default open at startup. - Templates: custom instructions for the AI. -- Settings persist in IndexedDB (local); API keys are not persisted on non‑local hosts. +- Settings persist in IndexedDB (local); API keys are not persisted on non‑local hosts unless you opt‑in via the "Persist API Keys on Non-Localhost" setting (not recommended for shared/production environments). ## 14. AI Agent "K.G.Studio Musician Assistant" - Open the chat (toolbar). Describe goals in natural language (e.g., “Can you help me write a 4‑bar chord progression for the melody?”). diff --git a/public/config.json b/public/config.json index 2527611..64a7de2 100644 --- a/public/config.json +++ b/public/config.json @@ -2,6 +2,7 @@ "general": { "language": "en_us", "llm_provider": "openai", + "persist_api_keys_non_localhost": false, "openai": { "api_key": "", "flex": false, diff --git a/src/components/settings/sections/GeneralSettings.tsx b/src/components/settings/sections/GeneralSettings.tsx index e7d825c..702adce 100644 --- a/src/components/settings/sections/GeneralSettings.tsx +++ b/src/components/settings/sections/GeneralSettings.tsx @@ -13,6 +13,7 @@ const GeneralSettings: React.FC = () => { const [claudeOpenRouterBaseUrl, setClaudeOpenRouterBaseUrl] = useState(''); const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState(''); const [openaiFlex, setOpenaiFlex] = useState(false); + const [persistApiKeysNonLocalhost, setPersistApiKeysNonLocalhost] = useState(false); const [compatibleKey, setCompatibleKey] = useState(''); const [compatibleBaseUrl, setCompatibleBaseUrl] = useState(''); const [compatibleModel, setCompatibleModel] = useState(''); @@ -45,6 +46,7 @@ const GeneralSettings: React.FC = () => { setOpenaiKey((configManager.get('general.openai.api_key') as string) || ''); setOpenaiModel((configManager.get('general.openai.model') as string) || ''); setOpenaiFlex((configManager.get('general.openai.flex') as boolean) ?? false); + setPersistApiKeysNonLocalhost((configManager.get('general.persist_api_keys_non_localhost') as boolean) ?? false); setGeminiKey((configManager.get('general.gemini.api_key') as string) || ''); setGeminiModel((configManager.get('general.gemini.model') as string) || ''); setClaudeKey((configManager.get('general.claude.api_key') as string) || ''); @@ -107,6 +109,17 @@ const GeneralSettings: React.FC = () => { } }; + const handlePersistApiKeysNonLocalhostChange = async (value: string) => { + const boolValue = value === 'yes'; + setPersistApiKeysNonLocalhost(boolValue); + try { + await configManager.set('general.persist_api_keys_non_localhost', boolValue); + console.log('Persist API Keys Non-Localhost changed to:', boolValue); + } catch (error) { + console.error('Failed to save Persist API Keys Non-Localhost:', error); + } + }; + const handleGeminiKeyChange = (value: string) => { setGeminiKey(value); debouncedSave('general.gemini.api_key', value); @@ -189,6 +202,23 @@ const GeneralSettings: React.FC = () => { + +
+ + +
+ When enabled, API keys will be saved to browser storage even on non-localhost environments. Warning: This may increase security vulnerability to XSS attacks. +
+
@@ -208,7 +238,9 @@ const GeneralSettings: React.FC = () => {
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' - : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'} + : persistApiKeysNonLocalhost + ? 'Keys are persisted locally (the IndexedDB in your browser).' + : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
@@ -323,7 +355,9 @@ const GeneralSettings: React.FC = () => {
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' - : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'} + : persistApiKeysNonLocalhost + ? 'Keys are persisted locally (the IndexedDB in your browser).' + : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
@@ -377,7 +411,9 @@ const GeneralSettings: React.FC = () => {
{isLocalEnvironment ? 'Keys are persisted locally (the IndexedDB in your browser).' - : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'} + : persistApiKeysNonLocalhost + ? 'Keys are persisted locally (the IndexedDB in your browser).' + : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
diff --git a/src/core/config/ConfigManager.ts b/src/core/config/ConfigManager.ts index 3833075..994635c 100644 --- a/src/core/config/ConfigManager.ts +++ b/src/core/config/ConfigManager.ts @@ -8,6 +8,7 @@ interface AppConfig { general: { language: string; llm_provider: 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible'; + persist_api_keys_non_localhost: boolean; openai: { api_key: string; flex: boolean; @@ -172,6 +173,7 @@ export class ConfigManager { general: { language: 'en_us', llm_provider: 'openai', + persist_api_keys_non_localhost: false, openai: { api_key: '', flex: false, @@ -283,11 +285,11 @@ export class ConfigManager { */ private async saveToStorage(): Promise { try { - // For security: if not running on a local host, do not persist API keys. - // We still keep them in memory (this.config) for runtime usage. - const configToPersist = this.isRunningOnLocalhost() - ? this.config - : this.getSanitizedConfigForStorage(); + const shouldSanitize = !this.isRunningOnLocalhost() && + !this.config.general.persist_api_keys_non_localhost; + const configToPersist = shouldSanitize + ? this.getSanitizedConfigForStorage() + : this.config; await this.storage.save( DB_CONSTANTS.DB_NAME, @@ -540,12 +542,12 @@ export class ConfigManager { * for persistence to storage in non-local environments. */ private getSanitizedConfigForStorage(): AppConfig { - // Deep copy to avoid mutating in-memory config const copied: AppConfig = JSON.parse(JSON.stringify(this.config)); if (copied?.general) { if (copied.general.openai) copied.general.openai.api_key = ''; if (copied.general.gemini) copied.general.gemini.api_key = ''; if (copied.general.claude) copied.general.claude.api_key = ''; + if (copied.general.claude_openrouter) copied.general.claude_openrouter.api_key = ''; if (copied.general.openai_compatible) copied.general.openai_compatible.api_key = ''; } return copied; diff --git a/vite.config.ts b/vite.config.ts index 5cce865..c951acf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,4 +16,7 @@ export default defineConfig({ define: { __APP_VERSION__: JSON.stringify(version), }, + server: { + allowedHosts: ['testlocal.com', '.testlocal.com', 'localhost', '127.0.0.1'], + }, })