feat: add option to persist API keys on non-localhost (opt-in security feature)
This commit is contained in:
+3
-3
@@ -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:
|
- K.G.Studio is fully client‑side. 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 OpenAI‑compatible)
|
- 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.
|
- 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
|
## 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.
|
- If loading stalls, the overlay will time out; refresh to retry downloading.
|
||||||
|
|
||||||
## 13. Settings
|
## 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.
|
- 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 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"
|
## 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?”).
|
- Open the chat (toolbar). Describe goals in natural language (e.g., “Can you help me write a 4‑bar chord progression for the melody?”).
|
||||||
|
|||||||
@@ -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 +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;
|
||||||
|
|||||||
@@ -16,4 +16,7 @@ export default defineConfig({
|
|||||||
define: {
|
define: {
|
||||||
__APP_VERSION__: JSON.stringify(version),
|
__APP_VERSION__: JSON.stringify(version),
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
allowedHosts: ['testlocal.com', '.testlocal.com', 'localhost', '127.0.0.1'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user