Files
DeskClaw/electron/utils/secure-storage.ts
2026-03-23 19:11:53 +08:00

314 lines
9.7 KiB
TypeScript

/**
* Provider Storage
* Manages provider configurations and API keys.
* This file remains the legacy compatibility layer while the app migrates to
* account-based provider storage and a dedicated secret-store abstraction.
*/
import { BUILTIN_PROVIDER_TYPES, type ProviderType } from './provider-registry';
import { getActiveOpenClawProviders } from './openclaw-auth';
import {
deleteProviderAccount,
getProviderAccount,
listProviderAccounts,
providerAccountToConfig,
providerConfigToAccount,
saveProviderAccount,
setDefaultProviderAccount,
} from '../services/providers/provider-store';
import { ensureProviderStoreMigrated } from '../services/providers/provider-migration';
import { getClawXProviderStore } from '../services/providers/store-instance';
import {
deleteProviderSecret,
getProviderSecret,
setProviderSecret,
} from '../services/secrets/secret-store';
import { getOpenClawProviderKeyForType } from './provider-keys';
/**
* Provider configuration
*/
export interface ProviderConfig {
id: string;
name: string;
type: ProviderType;
baseUrl?: string;
apiProtocol?: 'openai-completions' | 'openai-responses' | 'anthropic-messages';
headers?: Record<string, string>;
model?: string;
fallbackModels?: string[];
fallbackProviderIds?: string[];
enabled: boolean;
createdAt: string;
updatedAt: string;
}
// ==================== API Key Storage ====================
/**
* Store an API key
*/
export async function storeApiKey(providerId: string, apiKey: string): Promise<boolean> {
try {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
keys[providerId] = apiKey;
s.set('apiKeys', keys);
await setProviderSecret({
type: 'api_key',
accountId: providerId,
apiKey,
});
return true;
} catch (error) {
console.error('Failed to store API key:', error);
return false;
}
}
/**
* Retrieve an API key
*/
export async function getApiKey(providerId: string): Promise<string | null> {
try {
await ensureProviderStoreMigrated();
const secret = await getProviderSecret(providerId);
if (secret?.type === 'api_key') {
return secret.apiKey;
}
if (secret?.type === 'local') {
return secret.apiKey ?? null;
}
const s = await getClawXProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
return keys[providerId] || null;
} catch (error) {
console.error('Failed to retrieve API key:', error);
return null;
}
}
/**
* Delete an API key
*/
export async function deleteApiKey(providerId: string): Promise<boolean> {
try {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
delete keys[providerId];
s.set('apiKeys', keys);
await deleteProviderSecret(providerId);
return true;
} catch (error) {
console.error('Failed to delete API key:', error);
return false;
}
}
/**
* Check if an API key exists for a provider
*/
export async function hasApiKey(providerId: string): Promise<boolean> {
await ensureProviderStoreMigrated();
const secret = await getProviderSecret(providerId);
if (secret?.type === 'api_key') {
return true;
}
const s = await getClawXProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
return providerId in keys;
}
/**
* List all provider IDs that have stored keys
*/
export async function listStoredKeyIds(): Promise<string[]> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
return Object.keys(keys);
}
// ==================== Provider Configuration ====================
/**
* Save a provider configuration
*/
export async function saveProvider(config: ProviderConfig): Promise<void> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
providers[config.id] = config;
s.set('providers', providers);
const defaultProviderId = (s.get('defaultProvider') ?? null) as string | null;
await saveProviderAccount(
providerConfigToAccount(config, { isDefault: defaultProviderId === config.id }),
);
}
/**
* Get a provider configuration
*/
export async function getProvider(providerId: string): Promise<ProviderConfig | null> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
if (providers[providerId]) {
return providers[providerId];
}
const account = await getProviderAccount(providerId);
return account ? providerAccountToConfig(account) : null;
}
/**
* Get all provider configurations
*/
export async function getAllProviders(): Promise<ProviderConfig[]> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
const legacyProviders = Object.values(providers);
if (legacyProviders.length > 0) {
return legacyProviders;
}
const accounts = await listProviderAccounts();
return accounts.map(providerAccountToConfig);
}
/**
* Delete a provider configuration and its API key
*/
export async function deleteProvider(providerId: string): Promise<boolean> {
try {
await ensureProviderStoreMigrated();
// Delete the API key
await deleteApiKey(providerId);
// Delete the provider config
const s = await getClawXProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
delete providers[providerId];
s.set('providers', providers);
await deleteProviderAccount(providerId);
// Clear default if this was the default
if (s.get('defaultProvider') === providerId) {
s.delete('defaultProvider');
s.delete('defaultProviderAccountId');
}
return true;
} catch (error) {
console.error('Failed to delete provider:', error);
return false;
}
}
/**
* Set the default provider
*/
export async function setDefaultProvider(providerId: string): Promise<void> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
s.set('defaultProvider', providerId);
await setDefaultProviderAccount(providerId);
}
/**
* Get the default provider
*/
export async function getDefaultProvider(): Promise<string | undefined> {
await ensureProviderStoreMigrated();
const s = await getClawXProviderStore();
return (s.get('defaultProvider') as string | undefined)
?? (s.get('defaultProviderAccountId') as string | undefined);
}
/**
* Get provider with masked key info (for UI display)
*/
export async function getProviderWithKeyInfo(
providerId: string
): Promise<(ProviderConfig & { hasKey: boolean; keyMasked: string | null }) | null> {
const provider = await getProvider(providerId);
if (!provider) return null;
const apiKey = await getApiKey(providerId);
let keyMasked: string | null = null;
if (apiKey) {
if (apiKey.length > 12) {
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
} else {
keyMasked = '*'.repeat(apiKey.length);
}
}
return {
...provider,
hasKey: !!apiKey,
keyMasked,
};
}
/**
* Get all providers with key info (for UI display)
* Also synchronizes ClawX local provider list with OpenClaw's actual config.
*/
export async function getAllProvidersWithKeyInfo(): Promise<
Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }>
> {
const providers = await getAllProviders();
const results: Array<ProviderConfig & { hasKey: boolean; keyMasked: string | null }> = [];
const activeOpenClawProviders = await getActiveOpenClawProviders();
// When openclaw.json is deleted/missing, the active set is empty.
// In that case, all providers (including builtins) should be cleaned up.
const configMissing = activeOpenClawProviders.size === 0;
for (const provider of providers) {
// Sync check: If it's a custom/OAuth provider and it no longer exists in OpenClaw config
// (e.g. wiped by Gateway due to missing plugin, or manually deleted by user)
// we should remove it from ClawX UI to stay consistent.
const isBuiltin = (BUILTIN_PROVIDER_TYPES as readonly string[]).includes(provider.type);
// For custom/ollama providers, the OpenClaw config key is derived as
// "<type>-<suffix>" where suffix = first 8 chars of providerId with hyphens stripped.
// e.g. provider.id "custom-a1b2c3d4-..." → strip hyphens → "customa1b2c3d4..." → slice(0,8) → "customa1"
// → openClawKey = "custom-customa1"
// This must match getOpenClawProviderKey() in ipc-handlers.ts exactly.
const openClawKey = getOpenClawProviderKeyForType(provider.type, provider.id);
const isActive = activeOpenClawProviders.has(provider.type) || activeOpenClawProviders.has(provider.id) || activeOpenClawProviders.has(openClawKey);
if (configMissing || (!isBuiltin && !isActive)) {
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, hiding from UI`);
// Skip from display but don't delete from store — preserves API key
// associations so that restoring openclaw.json brings accounts back intact.
continue;
}
const apiKey = await getApiKey(provider.id);
let keyMasked: string | null = null;
if (apiKey) {
if (apiKey.length > 12) {
keyMasked = `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`;
} else {
keyMasked = '*'.repeat(apiKey.length);
}
}
results.push({
...provider,
hasKey: !!apiKey,
keyMasked,
});
}
return results;
}