Merge pull request #26 from KGAudioLab/feat/2026-01-17-add-option-allow-persist-api-key-on-non-localhost

Feat/2026 01 17 add option allow persist api key on non localhost
This commit is contained in:
Xiaohan-Tian
2026-01-17 14:05:35 -08:00
committed by GitHub
6 changed files with 59 additions and 15 deletions
+4 -3
View File
@@ -48,7 +48,7 @@ This project investigates how AI-human collaboration can enhance creative music-
- Click here to start using the app online: [K.G.Studio (kgaudiolab.github.io/kgstudio)](https://kgaudiolab.github.io/kgstudio) - Click here to start using the app online: [K.G.Studio (kgaudiolab.github.io/kgstudio)](https://kgaudiolab.github.io/kgstudio)
- [Click here to get a free OpenRouter API Key](https://openrouter.ai/keys) (you may need an OpenRouter account). - [Click here to get a free OpenRouter API Key](https://openrouter.ai/keys) (you may need an OpenRouter account).
- In **Settings ⚙️ → General → LLM Provider**, select **OpenAI Compatible**. - In **Settings ⚙️ → General → LLM Provider**, select **OpenAI Compatible**.
- In **OpenAI Compatible Server → Key**, paste your key. (Note: on nonlocalhost, your key isnt persisted; youll reenter it after refresh the page. Keep it safe.) - In **OpenAI Compatible Server → Key**, paste your key. (Note: on nonlocalhost, your key isn't persisted by default for security; you can enable "Persist API Keys on Non-Localhost" in Settings to persist them, though this may increase XSS risk.)
- In **OpenAI Compatible Server → Model**, enter `qwen/qwen3-30b-a3b:free` or `qwen/qwen3-235b-a22b:free`. (Note: these are free models [qwen3-30b-a3b](https://openrouter.ai/qwen/qwen3-30b-a3b:free) and [qwen3-235b-a22b](https://openrouter.ai/qwen/qwen3-235b-a22b:free); nonfree models may require billing; some model providers may retain your data, check their privacy policies; this project is not affiliated with OpenRouter or any model provider.) - In **OpenAI Compatible Server → Model**, enter `qwen/qwen3-30b-a3b:free` or `qwen/qwen3-235b-a22b:free`. (Note: these are free models [qwen3-30b-a3b](https://openrouter.ai/qwen/qwen3-30b-a3b:free) and [qwen3-235b-a22b](https://openrouter.ai/qwen/qwen3-235b-a22b:free); nonfree models may require billing; some model providers may retain your data, check their privacy policies; this project is not affiliated with OpenRouter or any model provider.)
- In **OpenAI Compatible Server → Base URL**, enter `https://openrouter.ai/api/v1/chat/completions`. - In **OpenAI Compatible Server → Base URL**, enter `https://openrouter.ai/api/v1/chat/completions`.
@@ -110,6 +110,7 @@ K.G.Studio loads defaults from `./public/config.json` (with an internal fallback
- **General** - **General**
- LLM provider: OpenAI, or OpenAIcompatible - LLM provider: OpenAI, or OpenAIcompatible
- API keys and models for the selected provider - API keys and models for the selected provider
- Persist API Keys on Non-Localhost: Enable to persist API keys on non-localhost environments (security opt-in, not recommended for shared/production environments)
- OpenAIcompatible base URL (for selfhosted gateways) - OpenAIcompatible base URL (for selfhosted gateways)
- Soundfont base URL (CDN for instrument samples) - Soundfont base URL (CDN for instrument samples)
- **Behavior** - **Behavior**
@@ -125,7 +126,7 @@ K.G.Studio loads defaults from `./public/config.json` (with an internal fallback
- Downloading instrument sound samples from the configured soundfont CDN - Downloading instrument sound samples from the configured soundfont CDN
- Communicating with the LLM provider you select (e.g., OpenAI or OpenAIcompatible services) - Communicating with the LLM provider you select (e.g., OpenAI or OpenAIcompatible services)
- Outside of the two cases above, the app functions locally. If you block those endpoints, the app still loads; instrument playback and AI features will not function until network access is restored. - Outside of the two cases above, the app functions locally. If you block those endpoints, the app still loads; instrument playback and AI features will not function until network access is restored.
- For security, when running from a nonlocal host we do not persist your API key in IndexedDB (to reduce XSS risk). Youll be prompted to enter it each time you start K.G.Studio. - For security, when running from a nonlocal host we do not persist your API key in IndexedDB by default (to reduce XSS risk). You'll be prompted to enter it each time you start K.G.Studio. To optin to persistence on nonlocal hosts, enable "Persist API Keys on Non-Localhost" in Settings (not recommended for shared/production environments).
## Using the App ## Using the App
@@ -223,7 +224,7 @@ OpenRouter is a platform that provides unified access to a wide range of languag
Currently, based on our evaluation, OpenAIs opensource models (`gptoss20b` and `gptoss120b`) are not yet compatible with the current agent implementation; support is planned. Currently, based on our evaluation, OpenAIs opensource models (`gptoss20b` and `gptoss120b`) are not yet compatible with the current agent implementation; support is planned.
For security, when using K.G.Studio from a nonlocal host, API keys are not persisted in IndexedDB; you will need to input your API key each time you start K.G.Studio. For security, when using K.G.Studio from a nonlocal host, API keys are not persisted in IndexedDB by default; you will need to input your API key each time you start K.G.Studio. To optin to persistence on nonlocal hosts, enable "Persist API Keys on Non-Localhost" in Settings (not recommended for shared/production environments).
K.G.Studio does not provide or host any of the models listed above, nor is it affiliated with any model provider. All data is stored locally on your device; K.G.Studio does not collect or transmit your data. You are solely responsible for any data you provide to thirdparty model providers. K.G.Studio does not provide or host any of the models listed above, nor is it affiliated with any model provider. All data is stored locally on your device; K.G.Studio does not collect or transmit your data. You are solely responsible for any data you provide to thirdparty model providers.
+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: - K.G.Studio is fully clientside. It connects to the network only to:
- Download instrument soundfonts from the configured CDN - Download instrument soundfonts from the configured CDN
- Communicate with your chosen LLM provider (OpenAI or OpenAIcompatible) - 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. - 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 ## 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. - If loading stalls, the overlay will time out; refresh to retry downloading.
## 13. Settings ## 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. - Behavior: chatbox default open at startup.
- Templates: custom instructions for the AI. - 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" ## 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?”). - 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": { "general": {
"language": "en_us", "language": "en_us",
"llm_provider": "openai", "llm_provider": "openai",
"persist_api_keys_non_localhost": false,
"openai": { "openai": {
"api_key": "", "api_key": "",
"flex": false, "flex": false,
@@ -13,6 +13,7 @@ const GeneralSettings: React.FC = () => {
const [claudeOpenRouterBaseUrl, setClaudeOpenRouterBaseUrl] = useState<string>(''); const [claudeOpenRouterBaseUrl, setClaudeOpenRouterBaseUrl] = useState<string>('');
const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState<string>(''); const [claudeOpenRouterModel, setClaudeOpenRouterModel] = useState<string>('');
const [openaiFlex, setOpenaiFlex] = useState<boolean>(false); const [openaiFlex, setOpenaiFlex] = useState<boolean>(false);
const [persistApiKeysNonLocalhost, setPersistApiKeysNonLocalhost] = useState<boolean>(false);
const [compatibleKey, setCompatibleKey] = useState<string>(''); const [compatibleKey, setCompatibleKey] = useState<string>('');
const [compatibleBaseUrl, setCompatibleBaseUrl] = useState<string>(''); const [compatibleBaseUrl, setCompatibleBaseUrl] = useState<string>('');
const [compatibleModel, setCompatibleModel] = useState<string>(''); const [compatibleModel, setCompatibleModel] = useState<string>('');
@@ -45,6 +46,7 @@ const GeneralSettings: React.FC = () => {
setOpenaiKey((configManager.get('general.openai.api_key') as string) || ''); setOpenaiKey((configManager.get('general.openai.api_key') as string) || '');
setOpenaiModel((configManager.get('general.openai.model') as string) || ''); setOpenaiModel((configManager.get('general.openai.model') as string) || '');
setOpenaiFlex((configManager.get('general.openai.flex') as boolean) ?? false); 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) || ''); setGeminiKey((configManager.get('general.gemini.api_key') as string) || '');
setGeminiModel((configManager.get('general.gemini.model') as string) || ''); setGeminiModel((configManager.get('general.gemini.model') as string) || '');
setClaudeKey((configManager.get('general.claude.api_key') 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) => { const handleGeminiKeyChange = (value: string) => {
setGeminiKey(value); setGeminiKey(value);
debouncedSave('general.gemini.api_key', 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> <option value="openai_compatible">OpenAI Compatible (e.g. OpenRouter, Ollama)</option>
</select> </select>
</div> </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>
<div className="settings-group"> <div className="settings-group">
@@ -207,6 +237,8 @@ const GeneralSettings: React.FC = () => {
/> />
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}> <div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{isLocalEnvironment {isLocalEnvironment
? 'Keys are persisted locally (the IndexedDB in your browser).'
: persistApiKeysNonLocalhost
? 'Keys are persisted locally (the IndexedDB in your browser).' ? '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.'} : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
</div> </div>
@@ -322,6 +354,8 @@ const GeneralSettings: React.FC = () => {
/> />
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}> <div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{isLocalEnvironment {isLocalEnvironment
? 'Keys are persisted locally (the IndexedDB in your browser).'
: persistApiKeysNonLocalhost
? 'Keys are persisted locally (the IndexedDB in your browser).' ? '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.'} : 'For security, keys are not persisted on non-local hosts and are kept in-memory for this session.'}
</div> </div>
@@ -376,6 +410,8 @@ const GeneralSettings: React.FC = () => {
/> />
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}> <div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
{isLocalEnvironment {isLocalEnvironment
? 'Keys are persisted locally (the IndexedDB in your browser).'
: persistApiKeysNonLocalhost
? 'Keys are persisted locally (the IndexedDB in your browser).' ? '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.'} : '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: { general: {
language: string; language: string;
llm_provider: 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible'; llm_provider: 'openai' | 'gemini' | 'claude' | 'claude_openrouter' | 'openai_compatible';
persist_api_keys_non_localhost: boolean;
openai: { openai: {
api_key: string; api_key: string;
flex: boolean; flex: boolean;
@@ -172,6 +173,7 @@ export class ConfigManager {
general: { general: {
language: 'en_us', language: 'en_us',
llm_provider: 'openai', llm_provider: 'openai',
persist_api_keys_non_localhost: false,
openai: { openai: {
api_key: '', api_key: '',
flex: false, flex: false,
@@ -283,11 +285,11 @@ export class ConfigManager {
*/ */
private async saveToStorage(): Promise<void> { private async saveToStorage(): Promise<void> {
try { try {
// For security: if not running on a local host, do not persist API keys. const shouldSanitize = !this.isRunningOnLocalhost() &&
// We still keep them in memory (this.config) for runtime usage. !this.config.general.persist_api_keys_non_localhost;
const configToPersist = this.isRunningOnLocalhost() const configToPersist = shouldSanitize
? this.config ? this.getSanitizedConfigForStorage()
: this.getSanitizedConfigForStorage(); : this.config;
await this.storage.save( await this.storage.save(
DB_CONSTANTS.DB_NAME, DB_CONSTANTS.DB_NAME,
@@ -540,12 +542,12 @@ export class ConfigManager {
* for persistence to storage in non-local environments. * for persistence to storage in non-local environments.
*/ */
private getSanitizedConfigForStorage(): AppConfig { private getSanitizedConfigForStorage(): AppConfig {
// Deep copy to avoid mutating in-memory config
const copied: AppConfig = JSON.parse(JSON.stringify(this.config)); const copied: AppConfig = JSON.parse(JSON.stringify(this.config));
if (copied?.general) { if (copied?.general) {
if (copied.general.openai) copied.general.openai.api_key = ''; if (copied.general.openai) copied.general.openai.api_key = '';
if (copied.general.gemini) copied.general.gemini.api_key = ''; if (copied.general.gemini) copied.general.gemini.api_key = '';
if (copied.general.claude) copied.general.claude.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 = ''; if (copied.general.openai_compatible) copied.general.openai_compatible.api_key = '';
} }
return copied; return copied;
+4
View File
@@ -16,4 +16,8 @@ export default defineConfig({
define: { define: {
__APP_VERSION__: JSON.stringify(version), __APP_VERSION__: JSON.stringify(version),
}, },
server: {
// IMPORTANT: MAKE SURE TO UPDATE YOUR OS HOSTS FILE TO POINT `testlocal.com` TO YOUR LOCAL IP (e.g. 127.0.0.1).
allowedHosts: ['testlocal.com', '.testlocal.com', 'localhost', '127.0.0.1'],
},
}) })