feat: add option to persist API keys on non-localhost (opt-in security feature)

This commit is contained in:
Xiaohan-Tian
2026-01-17 13:54:15 -08:00
parent 2a0d128d0f
commit e8b79efde7
5 changed files with 54 additions and 12 deletions
+3 -3
View File
@@ -36,7 +36,7 @@ A lightweight, browserbased DAW with an AI Agent "K.G.Studio Musician Assista
- K.G.Studio is fully clientside. It connects to the network only to:
- Download instrument soundfonts from the configured CDN
- Communicate with your chosen LLM provider (OpenAI or OpenAIcompatible)
- API keys are not persisted when running from a nonlocal host (to reduce XSS risk). Youll be prompted to reenter them on each start in that scenario.
- API keys are not persisted when running from a nonlocal host (to reduce XSS risk). You'll be prompted to reenter them on each start in that scenario. To optin to persistence on nonlocal 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 dataretention policies. Review the policy of the provider you select before use.
## 5. User Interface Tour
@@ -125,10 +125,10 @@ A lightweight, browserbased 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 OpenAIcompatible), API key, model, soundfont base URL.
- General: LLM provider (OpenAI or OpenAIcompatible), 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 nonlocal hosts.
- Settings persist in IndexedDB (local); API keys are not persisted on nonlocal hosts unless you optin 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 4bar chord progression for the melody?”).
+1
View File
@@ -2,6 +2,7 @@
"general": {
"language": "en_us",
"llm_provider": "openai",
"persist_api_keys_non_localhost": false,
"openai": {
"api_key": "",
"flex": false,
@@ -13,6 +13,7 @@ const GeneralSettings: React.FC = () => {
const [claudeOpenRouterBaseUrl, setClaudeOpenRouterBaseUrl] = useState<string>('');
const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState<string>('');
const [openaiFlex, setOpenaiFlex] = useState<boolean>(false);
const [persistApiKeysNonLocalhost, setPersistApiKeysNonLocalhost] = useState<boolean>(false);
const [compatibleKey, setCompatibleKey] = useState<string>('');
const [compatibleBaseUrl, setCompatibleBaseUrl] = useState<string>('');
const [compatibleModel, setCompatibleModel] = useState<string>('');
@@ -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 = () => {
<option value="openai_compatible">OpenAI Compatible (e.g. OpenRouter, Ollama)</option>
</select>
</div>
<div className="settings-item">
<label className="settings-label">
Persist API Keys on Non-Localhost
</label>
<select
className="settings-select"
value={persistApiKeysNonLocalhost ? 'yes' : 'no'}
onChange={(e) => handlePersistApiKeysNonLocalhostChange(e.target.value)}
>
<option value="no">No</option>
<option value="yes">Yes</option>
</select>
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
When enabled, API keys will be saved to browser storage even on non-localhost environments. Warning: This may increase security vulnerability to XSS attacks.
</div>
</div>
</div>
<div className="settings-group">
@@ -208,7 +238,9 @@ const GeneralSettings: React.FC = () => {
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{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.'}
</div>
</div>
@@ -323,7 +355,9 @@ const GeneralSettings: React.FC = () => {
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{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.'}
</div>
</div>
@@ -377,7 +411,9 @@ const GeneralSettings: React.FC = () => {
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{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.'}
</div>
</div>
+8 -6
View File
@@ -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<void> {
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;
+3
View File
@@ -16,4 +16,7 @@ export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(version),
},
server: {
allowedHosts: ['testlocal.com', '.testlocal.com', 'localhost', '127.0.0.1'],
},
})