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:
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
Journal,
|
||||
KeyCipher,
|
||||
KeyResolver,
|
||||
MediaStorage,
|
||||
ProviderRegistry,
|
||||
ProvidersStore,
|
||||
createNordRouterProviders,
|
||||
loadConfig,
|
||||
LocalWhisperProvider,
|
||||
@@ -16,10 +19,17 @@ export interface AppContext {
|
||||
registry: ProviderRegistry;
|
||||
storage: MediaStorage;
|
||||
journal: Journal;
|
||||
providersStore: ProvidersStore;
|
||||
/** Effective default provider name after resolving DB/env. */
|
||||
defaultProvider: string;
|
||||
}
|
||||
|
||||
/** Built-in providers always considered, even without a DB row. */
|
||||
const BUILTINS = [{ name: 'nordrouter', defaultBaseUrl: 'https://nordrouter.com' }];
|
||||
|
||||
/**
|
||||
* Wire up all providers, storage and the journal once at startup.
|
||||
* Wire up providers, storage, journal and the encrypted providers store.
|
||||
* API keys resolve with ENV > DB precedence (see KeyResolver).
|
||||
*/
|
||||
export function buildContext(): AppContext {
|
||||
const config = loadConfig();
|
||||
@@ -27,20 +37,50 @@ export function buildContext(): AppContext {
|
||||
const journal = new Journal(config.dbPath);
|
||||
const registry = new ProviderRegistry();
|
||||
|
||||
// NordRouter (media + search). Registered even without a key so error messages
|
||||
// are clear when a tool is actually invoked.
|
||||
try {
|
||||
const nr = createNordRouterProviders({ config: config.nordrouter, storage, save: true });
|
||||
registry.registerMedia(nr.media);
|
||||
registry.registerSearch(nr.search);
|
||||
log.info('registered nordrouter provider (media + search)');
|
||||
} catch (err) {
|
||||
log.warn('nordrouter not registered', err instanceof Error ? err.message : String(err));
|
||||
const cipher = new KeyCipher(config.masterKey);
|
||||
const providersStore = new ProvidersStore(config.providersDbPath, cipher);
|
||||
const resolver = new KeyResolver(config, providersStore);
|
||||
|
||||
if (!cipher.enabled) {
|
||||
log.warn('NEXUS_MASTER_KEY not set — DB-stored API keys are unavailable; using env keys only');
|
||||
}
|
||||
|
||||
const resolved = resolver.resolveAll(BUILTINS);
|
||||
const defaultProvider = resolver.resolveDefault(resolved);
|
||||
|
||||
for (const p of resolved) {
|
||||
if (!p.enabled) {
|
||||
log.info(`provider "${p.name}" disabled — skipped`);
|
||||
continue;
|
||||
}
|
||||
if (p.type === 'nordrouter') {
|
||||
if (!p.apiKey) {
|
||||
log.warn(`provider "${p.name}" has no API key (env or DB) — tools will error when used`);
|
||||
}
|
||||
try {
|
||||
const nr = createNordRouterProviders({
|
||||
config: { apiKey: p.apiKey, baseUrl: p.baseUrl },
|
||||
storage,
|
||||
save: true,
|
||||
});
|
||||
// Register under the provider's own name so multiple nordrouter-type
|
||||
// providers with different keys can coexist.
|
||||
Object.defineProperty(nr.media, 'name', { value: p.name });
|
||||
Object.defineProperty(nr.search, 'name', { value: p.name });
|
||||
registry.registerMedia(nr.media);
|
||||
registry.registerSearch(nr.search);
|
||||
log.info(`registered provider "${p.name}" (media + search, key: ${p.keySource})`);
|
||||
} catch (err) {
|
||||
log.warn(`failed to register "${p.name}"`, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
} else {
|
||||
log.warn(`provider type "${p.type}" (${p.name}) not implemented yet — skipped`);
|
||||
}
|
||||
}
|
||||
|
||||
// Local STT (faster-whisper sidecar).
|
||||
registry.registerStt(new LocalWhisperProvider(config.stt));
|
||||
log.info('registered local STT provider');
|
||||
log.info(`default provider: ${defaultProvider}`);
|
||||
|
||||
return { config, registry, storage, journal };
|
||||
return { config, registry, storage, journal, providersStore, defaultProvider };
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class Handlers {
|
||||
constructor(private readonly ctx: AppContext) {}
|
||||
|
||||
private providerName(explicit?: string): string {
|
||||
return explicit || this.ctx.config.defaultProvider;
|
||||
return explicit || this.ctx.defaultProvider;
|
||||
}
|
||||
|
||||
async handle(name: string, args: Record<string, unknown>): Promise<McpResult> {
|
||||
|
||||
Reference in New Issue
Block a user