feat(app): add encrypted API key vault with IDB storage

Create key-vault.ts with AES-256-GCM encrypted IndexedDB storage for
API keys (Claude, OpenRouter). Add ApiKeyManager.vue settings UI with
masked key display and add/remove functionality. Integrate vault lookups
into useAI.ts streaming functions with graceful fallback when IDB is
unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 20:57:39 +00:00
co-authored by Claude Opus 4.6
parent 621a324859
commit 9e4a2c30e1
3 changed files with 292 additions and 6 deletions
+23 -6
View File
@@ -1,6 +1,7 @@
import { ref, computed } from 'vue'
import { useChatStore } from '@/stores/chat'
import { searchWeb } from '@/composables/useWebSearch'
import { getApiKey } from '@/utils/key-vault'
type Provider = 'claude' | 'openrouter' | 'mock'
@@ -136,9 +137,17 @@ async function streamClaude(
webSearch: boolean,
signal?: AbortSignal,
): Promise<void> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
// Use vault key if available, proxy uses its own key as fallback
const vaultKey = await getApiKey('claude')
if (vaultKey) {
headers['x-api-key'] = vaultKey
}
const res = await fetch(CLAUDE_PATH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify({
model: activeModel.value,
system: systemPrompt,
@@ -177,13 +186,21 @@ async function streamOpenRouter(
...messages.map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content })),
]
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin,
'X-Title': 'AIUI',
}
// Use vault key if available, otherwise proxy handles auth
const vaultKey = await getApiKey('openrouter')
if (vaultKey) {
headers['Authorization'] = `Bearer ${vaultKey}`
}
const res = await fetch(OPENROUTER_PATH, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'HTTP-Referer': window.location.origin,
'X-Title': 'AIUI',
},
headers,
body: JSON.stringify({
model: activeModel.value,
messages: orMessages,