Add managed provider keys: encrypted store + env>DB resolver

AES-256-GCM encryption (KeyCipher) with master key from NEXUS_MASTER_KEY,
never persisted. ProvidersStore in SQLite: CRUD, enable/disable, default,
encrypted api keys, masked views for UI. KeyResolver applies env>DB precedence
and supports multiple providers. context.ts registers providers dynamically
from resolved config; backward compatible with env-only setups.
This commit is contained in:
OpenCode
2026-08-17 17:12:26 +07:00
parent 02fc536e85
commit 62c692c5a1
9 changed files with 469 additions and 13 deletions
+26
View File
@@ -114,6 +114,32 @@ npm run build
- **Журнал** SQLite (`nexus.sqlite` в media-dir) пишет каждую операцию; `nexus_usage_stats` агрегирует траты.
- MCP работает по stdio — **все логи идут в stderr**.
## Ключи провайдеров
Ключи можно задавать двумя способами, с приоритетом **env → БД**:
1. **Env (приоритет)**`NORDROUTER_API_KEY``<NAME>_API_KEY` для других провайдеров). Просто для CI и локального запуска.
2. **Зашифрованная БД** — управляется из будущей админки. Ключи хранятся в SQLite, зашифрованные **AES-256-GCM** на мастер-ключе `NEXUS_MASTER_KEY` (сам мастер-ключ — только в env, в БД его нет).
Свойства слоя:
- В git/логи/БД сырой ключ не попадает; в UI показывается только маска `sk-…last4`.
- Бэкап БД не раскрывает ключи (нужен мастер-ключ).
- Ротация `NEXUS_MASTER_KEY` инвалидирует зашифрованные ключи — их нужно ввести заново.
- Без `NEXUS_MASTER_KEY` работают только env-ключи (сервер предупреждает в stderr).
Программное управление (пока до админки) — через `@nexusai/core`:
```ts
import { KeyCipher, ProvidersStore } from '@nexusai/core';
const cipher = new KeyCipher(process.env.NEXUS_MASTER_KEY);
const store = new ProvidersStore('./media-output/nexus.sqlite', cipher);
store.upsert({ name: 'nordrouter', type: 'nordrouter', apiKey: 'sk-...', isDefault: true });
store.setEnabled('nordrouter', true);
store.listViews(); // masked view for UI
```
Генерация мастер-ключа: `openssl rand -hex 32`.
## STT в ограниченных сетях
faster-whisper скачивает модель с HuggingFace. Если `huggingface.co` недоступен: