-
Notifications
You must be signed in to change notification settings - Fork 992
Expand file tree
/
Copy pathsecure-storage.ts
More file actions
264 lines (232 loc) · 7.79 KB
/
secure-storage.ts
File metadata and controls
264 lines (232 loc) · 7.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/**
* Provider Storage
* Manages provider configurations and API keys.
* Keys are stored in plain text alongside provider configs in a single electron-store.
*/
import type { ProviderType } from './provider-registry';
import { getActiveOpenClawProviders } from './openclaw-auth';
// Lazy-load electron-store (ESM module)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let providerStore: any = null;
async function getProviderStore() {
if (!providerStore) {
const Store = (await import('electron-store')).default;
providerStore = new Store({
name: 'clawx-providers',
defaults: {
providers: {} as Record<string, ProviderConfig>,
apiKeys: {} as Record<string, string>,
defaultProvider: null as string | null,
},
});
}
return providerStore;
}
/**
* Provider configuration
*/
export interface ProviderConfig {
id: string;
name: string;
type: ProviderType;
baseUrl?: string;
model?: 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 {
const s = await getProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
keys[providerId] = apiKey;
s.set('apiKeys', keys);
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 {
const s = await getProviderStore();
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 {
const s = await getProviderStore();
const keys = (s.get('apiKeys') || {}) as Record<string, string>;
delete keys[providerId];
s.set('apiKeys', keys);
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> {
const s = await getProviderStore();
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[]> {
const s = await getProviderStore();
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> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
providers[config.id] = config;
s.set('providers', providers);
}
/**
* Get a provider configuration
*/
export async function getProvider(providerId: string): Promise<ProviderConfig | null> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
return providers[providerId] || null;
}
/**
* Get all provider configurations
*/
export async function getAllProviders(): Promise<ProviderConfig[]> {
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
return Object.values(providers);
}
/**
* Delete a provider configuration and its API key
*/
export async function deleteProvider(providerId: string): Promise<boolean> {
try {
// Delete the API key
await deleteApiKey(providerId);
// Delete the provider config
const s = await getProviderStore();
const providers = s.get('providers') as Record<string, ProviderConfig>;
delete providers[providerId];
s.set('providers', providers);
// Clear default if this was the default
if (s.get('defaultProvider') === providerId) {
s.delete('defaultProvider');
}
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> {
const s = await getProviderStore();
s.set('defaultProvider', providerId);
}
/**
* Get the default provider
*/
export async function getDefaultProvider(): Promise<string | undefined> {
const s = await getProviderStore();
return s.get('defaultProvider') 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 = getActiveOpenClawProviders();
// We need to avoid deleting native ones like 'anthropic' or 'google'
// that don't need to exist in openclaw.json models.providers
const OpenClawBuiltinList = [
'anthropic', 'openai', 'google', 'moonshot', 'siliconflow', 'ollama'
];
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 = OpenClawBuiltinList.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 = (provider.type === 'custom' || provider.type === 'ollama')
? `${provider.type}-${provider.id.replace(/-/g, '').slice(0, 8)}`
: provider.type === 'minimax-portal-cn' ? 'minimax-portal' : provider.type;
if (!isBuiltin && !activeOpenClawProviders.has(provider.type) && !activeOpenClawProviders.has(provider.id) && !activeOpenClawProviders.has(openClawKey)) {
console.log(`[Sync] Provider ${provider.id} (${provider.type}) missing from OpenClaw, dropping from ClawX UI`);
await deleteProvider(provider.id);
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;
}