mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-19 08:14:52 +00:00
feat(websearch): manage provider API keys in dashboard
- add masked dashboard-managed API key state for Exa, Tavily, and Brave - persist WebSearch keys through global_env while keeping WebSearch as the UX surface - treat dashboard-managed keys as ready in status checks and cover the flow with route/UI tests
This commit is contained in:
1 parent
eea0f57688
commit
20c48c6105
10 files changed
+775
-31
No files matched your search
@@ -18,6 +18,8 @@ export type {
|
||||
WebSearchConfig,
|
||||
} from './types';
|
||||
|
||||
export type { WebSearchApiKeyState } from './provider-secrets';
|
||||
|
||||
// Gemini CLI
|
||||
export {
|
||||
getGeminiCliStatus,
|
||||
@@ -56,5 +58,7 @@ export {
|
||||
displayWebSearchStatus,
|
||||
} from './status';
|
||||
|
||||
export { WEBSEARCH_API_KEY_PROVIDERS, getWebSearchApiKeyStates } from './provider-secrets';
|
||||
|
||||
// Profile Hook Injection
|
||||
export { ensureProfileHooks, removeMigrationMarker } from './profile-hook-injector';
|
||||
@@ -0,0 +1,132 @@
|
||||
import { getGlobalEnvConfig } from '../../config/unified-config-loader';
|
||||
import { maskSensitiveValue } from '../sensitive-keys';
|
||||
|
||||
export type WebSearchApiKeyProviderId = 'exa' | 'tavily' | 'brave';
|
||||
export type WebSearchApiKeySource = 'global_env' | 'process_env' | 'both' | 'none';
|
||||
|
||||
export interface WebSearchApiKeyState {
|
||||
envVar: string;
|
||||
configured: boolean;
|
||||
available: boolean;
|
||||
source: WebSearchApiKeySource;
|
||||
maskedValue?: string;
|
||||
}
|
||||
|
||||
interface WebSearchApiKeyDescriptor {
|
||||
envVar: string;
|
||||
alternateEnvVar: string;
|
||||
}
|
||||
|
||||
export const WEBSEARCH_API_KEY_PROVIDERS: Record<
|
||||
WebSearchApiKeyProviderId,
|
||||
WebSearchApiKeyDescriptor
|
||||
> = {
|
||||
exa: {
|
||||
envVar: 'EXA_API_KEY',
|
||||
alternateEnvVar: 'CCS_WEBSEARCH_EXA_API_KEY',
|
||||
},
|
||||
tavily: {
|
||||
envVar: 'TAVILY_API_KEY',
|
||||
alternateEnvVar: 'CCS_WEBSEARCH_TAVILY_API_KEY',
|
||||
},
|
||||
brave: {
|
||||
envVar: 'BRAVE_API_KEY',
|
||||
alternateEnvVar: 'CCS_WEBSEARCH_BRAVE_API_KEY',
|
||||
},
|
||||
};
|
||||
|
||||
function getTrimmedEnvValue(
|
||||
env: Record<string, string | undefined>,
|
||||
names: string[]
|
||||
): string | undefined {
|
||||
for (const name of names) {
|
||||
const value = env[name]?.trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveSource(
|
||||
hasGlobalEnvValue: boolean,
|
||||
hasProcessEnvValue: boolean
|
||||
): WebSearchApiKeySource {
|
||||
if (hasGlobalEnvValue && hasProcessEnvValue) {
|
||||
return 'both';
|
||||
}
|
||||
if (hasGlobalEnvValue) {
|
||||
return 'global_env';
|
||||
}
|
||||
if (hasProcessEnvValue) {
|
||||
return 'process_env';
|
||||
}
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export function getWebSearchApiKeyStates(): Record<
|
||||
WebSearchApiKeyProviderId,
|
||||
WebSearchApiKeyState
|
||||
> {
|
||||
const globalEnvConfig = getGlobalEnvConfig();
|
||||
const storedEnv = globalEnvConfig.env ?? {};
|
||||
|
||||
return (
|
||||
Object.entries(WEBSEARCH_API_KEY_PROVIDERS) as Array<
|
||||
[WebSearchApiKeyProviderId, WebSearchApiKeyDescriptor]
|
||||
>
|
||||
).reduce(
|
||||
(acc, [providerId, descriptor]) => {
|
||||
const names = [descriptor.envVar, descriptor.alternateEnvVar];
|
||||
const globalEnvValue = getTrimmedEnvValue(storedEnv, names);
|
||||
const processEnvValue = getTrimmedEnvValue(process.env, names);
|
||||
const configured = Boolean(globalEnvValue || processEnvValue);
|
||||
const available = Boolean(processEnvValue || (globalEnvConfig.enabled && globalEnvValue));
|
||||
const visibleValue = globalEnvValue || processEnvValue;
|
||||
|
||||
acc[providerId] = {
|
||||
envVar: descriptor.envVar,
|
||||
configured,
|
||||
available,
|
||||
source: resolveSource(Boolean(globalEnvValue), Boolean(processEnvValue)),
|
||||
maskedValue: visibleValue ? maskSensitiveValue(visibleValue) : undefined,
|
||||
};
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<WebSearchApiKeyProviderId, WebSearchApiKeyState>
|
||||
);
|
||||
}
|
||||
|
||||
export function applyWebSearchApiKeyUpdates(
|
||||
env: Record<string, string>,
|
||||
apiKeys: Partial<Record<WebSearchApiKeyProviderId, string | null>> | undefined
|
||||
): Record<string, string> {
|
||||
if (!apiKeys) {
|
||||
return env;
|
||||
}
|
||||
|
||||
const nextEnv = { ...env };
|
||||
|
||||
for (const [providerId, rawValue] of Object.entries(apiKeys) as Array<
|
||||
[WebSearchApiKeyProviderId, string | null | undefined]
|
||||
>) {
|
||||
if (rawValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const descriptor = WEBSEARCH_API_KEY_PROVIDERS[providerId];
|
||||
delete nextEnv[descriptor.alternateEnvVar];
|
||||
|
||||
const normalizedValue = typeof rawValue === 'string' ? rawValue.trim() : '';
|
||||
if (!normalizedValue) {
|
||||
delete nextEnv[descriptor.envVar];
|
||||
continue;
|
||||
}
|
||||
|
||||
nextEnv[descriptor.envVar] = normalizedValue;
|
||||
}
|
||||
|
||||
return nextEnv;
|
||||
}
|
||||
@@ -11,16 +11,13 @@ import { getWebSearchConfig } from '../../config/unified-config-loader';
|
||||
import { getGeminiCliStatus, isGeminiAuthenticated } from './gemini-cli';
|
||||
import { getGrokCliStatus } from './grok-cli';
|
||||
import { getOpenCodeCliStatus } from './opencode-cli';
|
||||
import { getWebSearchApiKeyStates } from './provider-secrets';
|
||||
import type { WebSearchCliInfo, WebSearchStatus } from './types';
|
||||
|
||||
function hasEnvValue(name: string): boolean {
|
||||
return (process.env[name] || '').trim().length > 0;
|
||||
}
|
||||
|
||||
function hasAnyEnvValue(names: string[]): boolean {
|
||||
return names.some((name) => hasEnvValue(name));
|
||||
}
|
||||
|
||||
function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
const geminiStatus = getGeminiCliStatus();
|
||||
@@ -88,40 +85,41 @@ function getLegacyProviderStatuses(): WebSearchCliInfo[] {
|
||||
*/
|
||||
export function getWebSearchCliProviders(): WebSearchCliInfo[] {
|
||||
const wsConfig = getWebSearchConfig();
|
||||
const apiKeyStates = getWebSearchApiKeyStates();
|
||||
const providers: WebSearchCliInfo[] = [
|
||||
{
|
||||
id: 'exa',
|
||||
kind: 'backend',
|
||||
name: 'Exa',
|
||||
enabled: wsConfig.providers?.exa?.enabled ?? false,
|
||||
available:
|
||||
(wsConfig.providers?.exa?.enabled ?? false) &&
|
||||
hasAnyEnvValue(['EXA_API_KEY', 'CCS_WEBSEARCH_EXA_API_KEY']),
|
||||
available: (wsConfig.providers?.exa?.enabled ?? false) && apiKeyStates.exa.available,
|
||||
version: null,
|
||||
docsUrl: 'https://docs.exa.ai/reference/search',
|
||||
requiresApiKey: true,
|
||||
apiKeyEnvVar: 'EXA_API_KEY',
|
||||
description: 'API-backed search with strong relevance and content extraction.',
|
||||
detail: hasAnyEnvValue(['EXA_API_KEY', 'CCS_WEBSEARCH_EXA_API_KEY'])
|
||||
detail: apiKeyStates.exa.available
|
||||
? `API key detected (${wsConfig.providers?.exa?.max_results ?? 5} results)`
|
||||
: 'Set EXA_API_KEY',
|
||||
: apiKeyStates.exa.configured
|
||||
? 'Stored in dashboard, but Global Env is disabled'
|
||||
: 'Set EXA_API_KEY',
|
||||
},
|
||||
{
|
||||
id: 'tavily',
|
||||
kind: 'backend',
|
||||
name: 'Tavily',
|
||||
enabled: wsConfig.providers?.tavily?.enabled ?? false,
|
||||
available:
|
||||
(wsConfig.providers?.tavily?.enabled ?? false) &&
|
||||
hasAnyEnvValue(['TAVILY_API_KEY', 'CCS_WEBSEARCH_TAVILY_API_KEY']),
|
||||
available: (wsConfig.providers?.tavily?.enabled ?? false) && apiKeyStates.tavily.available,
|
||||
version: null,
|
||||
docsUrl: 'https://docs.tavily.com/documentation/api-reference/endpoint/search',
|
||||
requiresApiKey: true,
|
||||
apiKeyEnvVar: 'TAVILY_API_KEY',
|
||||
description: 'Search API optimized for agent workflows and concise web result synthesis.',
|
||||
detail: hasAnyEnvValue(['TAVILY_API_KEY', 'CCS_WEBSEARCH_TAVILY_API_KEY'])
|
||||
detail: apiKeyStates.tavily.available
|
||||
? `API key detected (${wsConfig.providers?.tavily?.max_results ?? 5} results)`
|
||||
: 'Set TAVILY_API_KEY',
|
||||
: apiKeyStates.tavily.configured
|
||||
? 'Stored in dashboard, but Global Env is disabled'
|
||||
: 'Set TAVILY_API_KEY',
|
||||
},
|
||||
{
|
||||
id: 'duckduckgo',
|
||||
@@ -140,17 +138,17 @@ export function getWebSearchCliProviders(): WebSearchCliInfo[] {
|
||||
kind: 'backend',
|
||||
name: 'Brave Search',
|
||||
enabled: wsConfig.providers?.brave?.enabled ?? false,
|
||||
available:
|
||||
(wsConfig.providers?.brave?.enabled ?? false) &&
|
||||
hasAnyEnvValue(['BRAVE_API_KEY', 'CCS_WEBSEARCH_BRAVE_API_KEY']),
|
||||
available: (wsConfig.providers?.brave?.enabled ?? false) && apiKeyStates.brave.available,
|
||||
version: null,
|
||||
docsUrl: 'https://brave.com/search/api/',
|
||||
requiresApiKey: true,
|
||||
apiKeyEnvVar: 'BRAVE_API_KEY',
|
||||
description: 'API-backed web search with cleaner result metadata.',
|
||||
detail: hasAnyEnvValue(['BRAVE_API_KEY', 'CCS_WEBSEARCH_BRAVE_API_KEY'])
|
||||
detail: apiKeyStates.brave.available
|
||||
? `API key detected (${wsConfig.providers?.brave?.max_results ?? 5} results)`
|
||||
: 'Set BRAVE_API_KEY',
|
||||
: apiKeyStates.brave.configured
|
||||
? 'Stored in dashboard, but Global Env is disabled'
|
||||
: 'Set BRAVE_API_KEY',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user