204 lines
5.4 KiB
TypeScript
204 lines
5.4 KiB
TypeScript
/**
|
|
* localStorage utility for API settings with encryption
|
|
* API keys are encrypted using AES-256-GCM before storage
|
|
*/
|
|
|
|
import { encryptObject, decryptObject, isEncrypted, clearCryptoData } from "./crypto";
|
|
|
|
export interface ApiSettings {
|
|
// Required providers
|
|
openai_api_key: string;
|
|
alpha_vantage_api_key: string;
|
|
|
|
// Optional providers
|
|
anthropic_api_key: string;
|
|
google_api_key: string;
|
|
grok_api_key: string;
|
|
deepseek_api_key: string;
|
|
qwen_api_key: string;
|
|
finmind_api_key: string; // 台灣股市資料 API
|
|
|
|
// Custom endpoint
|
|
custom_base_url: string;
|
|
custom_api_key: string;
|
|
}
|
|
|
|
const STORAGE_KEY = "tradingagents_api_settings";
|
|
const ENCRYPTED_FLAG_KEY = "tradingagents_encrypted";
|
|
|
|
export const DEFAULT_API_SETTINGS: ApiSettings = {
|
|
openai_api_key: "",
|
|
alpha_vantage_api_key: "",
|
|
anthropic_api_key: "",
|
|
google_api_key: "",
|
|
grok_api_key: "",
|
|
deepseek_api_key: "",
|
|
qwen_api_key: "",
|
|
finmind_api_key: "",
|
|
custom_base_url: "",
|
|
custom_api_key: "",
|
|
};
|
|
|
|
/**
|
|
* Check if stored data is using legacy (unencrypted) format
|
|
*/
|
|
function isLegacyFormat(): boolean {
|
|
if (typeof window === "undefined") return false;
|
|
|
|
const encryptedFlag = localStorage.getItem(ENCRYPTED_FLAG_KEY);
|
|
if (encryptedFlag === "true") return false;
|
|
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (!stored) return false;
|
|
|
|
try {
|
|
const parsed = JSON.parse(stored);
|
|
// Check if any API key looks like plaintext (starts with known prefixes)
|
|
const apiKeyFields = Object.keys(parsed).filter(k => k.includes("api_key"));
|
|
for (const field of apiKeyFields) {
|
|
const value = parsed[field];
|
|
if (value && typeof value === "string") {
|
|
// OpenAI keys start with sk-, Anthropic with sk-ant-, etc.
|
|
if (value.startsWith("sk-") || value.startsWith("AIza") || value.length < 50) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get API settings from localStorage (async due to decryption)
|
|
*/
|
|
export async function getApiSettingsAsync(): Promise<ApiSettings> {
|
|
if (typeof window === "undefined") {
|
|
return DEFAULT_API_SETTINGS;
|
|
}
|
|
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (!stored) {
|
|
return DEFAULT_API_SETTINGS;
|
|
}
|
|
|
|
const parsed = JSON.parse(stored);
|
|
|
|
// If legacy format detected, return as-is (will be encrypted on next save)
|
|
if (isLegacyFormat()) {
|
|
console.warn("Legacy unencrypted settings detected. Will encrypt on next save.");
|
|
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
|
}
|
|
|
|
// Decrypt the settings
|
|
const decrypted = await decryptObject(parsed);
|
|
return { ...DEFAULT_API_SETTINGS, ...decrypted };
|
|
} catch (error) {
|
|
console.error("Error reading API settings:", error);
|
|
return DEFAULT_API_SETTINGS;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get API settings synchronously (for backward compatibility)
|
|
* WARNING: This returns encrypted values - use getApiSettingsAsync for actual values
|
|
*/
|
|
export function getApiSettings(): ApiSettings {
|
|
if (typeof window === "undefined") {
|
|
return DEFAULT_API_SETTINGS;
|
|
}
|
|
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored);
|
|
return { ...DEFAULT_API_SETTINGS, ...parsed };
|
|
}
|
|
} catch (error) {
|
|
console.error("Error reading API settings from localStorage:", error);
|
|
}
|
|
|
|
return DEFAULT_API_SETTINGS;
|
|
}
|
|
|
|
/**
|
|
* Save API settings to localStorage with encryption
|
|
*/
|
|
export async function saveApiSettingsAsync(settings: ApiSettings): Promise<void> {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Encrypt sensitive fields
|
|
const encrypted = await encryptObject(settings as unknown as Record<string, string>);
|
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(encrypted));
|
|
localStorage.setItem(ENCRYPTED_FLAG_KEY, "true");
|
|
} catch (error) {
|
|
console.error("Error saving API settings:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save API settings synchronously (legacy - not recommended)
|
|
* WARNING: This saves unencrypted data - use saveApiSettingsAsync instead
|
|
*/
|
|
export function saveApiSettings(settings: ApiSettings): void {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
// Call async version and ignore the promise (for backward compatibility)
|
|
saveApiSettingsAsync(settings).catch(console.error);
|
|
}
|
|
|
|
/**
|
|
* Clear API settings from localStorage
|
|
*/
|
|
export function clearApiSettings(): void {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
localStorage.removeItem(ENCRYPTED_FLAG_KEY);
|
|
clearCryptoData();
|
|
} catch (error) {
|
|
console.error("Error clearing API settings from localStorage:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrate legacy unencrypted settings to encrypted format
|
|
*/
|
|
export async function migrateToEncrypted(): Promise<boolean> {
|
|
if (typeof window === "undefined") return false;
|
|
|
|
if (!isLegacyFormat()) {
|
|
return false; // Already encrypted or no data
|
|
}
|
|
|
|
try {
|
|
const stored = localStorage.getItem(STORAGE_KEY);
|
|
if (!stored) return false;
|
|
|
|
const parsed = JSON.parse(stored);
|
|
const merged = { ...DEFAULT_API_SETTINGS, ...parsed };
|
|
|
|
// Save with encryption
|
|
await saveApiSettingsAsync(merged);
|
|
|
|
console.log("Successfully migrated settings to encrypted format");
|
|
return true;
|
|
} catch (error) {
|
|
console.error("Failed to migrate settings:", error);
|
|
return false;
|
|
}
|
|
}
|