feat: add option to persist API keys on non-localhost (opt-in security feature)
This commit is contained in:
@@ -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 +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;
|
||||
|
||||
Reference in New Issue
Block a user