@enactprotocol/shared 1.2.3 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,11 @@ import { EnactToolDefinition, ToolUsage, ToolSearchQuery, CLITokenCreate, OAuthT
2
2
  export declare class EnactApiClient {
3
3
  baseUrl: string;
4
4
  supabaseUrl: string;
5
- constructor(baseUrl?: string, supabaseUrl?: string);
5
+ constructor(baseUrl: string, supabaseUrl: string);
6
+ /**
7
+ * Create API client with config-based URLs
8
+ */
9
+ static create(baseUrl?: string, supabaseUrl?: string): Promise<EnactApiClient>;
6
10
  private makeRequest;
7
11
  /**
8
12
  * Get all tools (public, no auth required)
@@ -115,6 +119,7 @@ export declare class EnactApiClient {
115
119
  errors: string[];
116
120
  };
117
121
  }
122
+ export declare function createDefaultApiClient(): Promise<EnactApiClient>;
118
123
  export declare const enactApi: EnactApiClient;
119
124
  export declare class EnactApiError extends Error {
120
125
  statusCode?: number | undefined;
@@ -1,8 +1,17 @@
1
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
1
2
  export class EnactApiClient {
2
- constructor(baseUrl = "https://enact.tools", supabaseUrl = "https://xjnhhxwxovjifdxdwzih.supabase.co") {
3
+ constructor(baseUrl, supabaseUrl) {
3
4
  this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash
4
5
  this.supabaseUrl = supabaseUrl.replace(/\/$/, "");
5
6
  }
7
+ /**
8
+ * Create API client with config-based URLs
9
+ */
10
+ static async create(baseUrl, supabaseUrl) {
11
+ const frontendUrl = baseUrl || await getFrontendUrl();
12
+ const apiUrl = supabaseUrl || await getApiUrl();
13
+ return new EnactApiClient(frontendUrl, apiUrl);
14
+ }
6
15
  // Helper method to make authenticated requests
7
16
  async makeRequest(endpoint, options = {}, token, tokenType = "jwt") {
8
17
  const url = endpoint.startsWith("http")
@@ -389,8 +398,12 @@ export class EnactApiClient {
389
398
  };
390
399
  }
391
400
  }
392
- // Export a default instance
393
- export const enactApi = new EnactApiClient();
401
+ // Export a default instance factory
402
+ export async function createDefaultApiClient() {
403
+ return await EnactApiClient.create();
404
+ }
405
+ // Keep backward compatibility with sync usage
406
+ export const enactApi = new EnactApiClient("https://enact.tools", "https://xjnhhxwxovjifdxdwzih.supabase.co");
394
407
  // Export error types for better error handling
395
408
  export class EnactApiError extends Error {
396
409
  constructor(message, statusCode, endpoint) {
@@ -402,5 +415,7 @@ export class EnactApiError extends Error {
402
415
  }
403
416
  // Helper function to create API client with custom configuration
404
417
  export function createEnactApiClient(baseUrl, supabaseUrl) {
405
- return new EnactApiClient(baseUrl, supabaseUrl);
418
+ const defaultFrontend = "https://enact.tools";
419
+ const defaultApi = "https://xjnhhxwxovjifdxdwzih.supabase.co";
420
+ return new EnactApiClient(baseUrl || defaultFrontend, supabaseUrl || defaultApi);
406
421
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Shared constants for Enact CLI
3
+ */
4
+ export declare const DEFAULT_FRONTEND_URL = "https://enact.tools";
5
+ export declare const DEFAULT_API_URL = "https://xjnhhxwxovjifdxdwzih.supabase.co";
6
+ export declare const ENV_FRONTEND_URL = "ENACT_FRONTEND_URL";
7
+ export declare const ENV_API_URL = "ENACT_API_URL";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared constants for Enact CLI
3
+ */
4
+ // Frontend URL - used for OAuth redirects, registry browsing, documentation links
5
+ export const DEFAULT_FRONTEND_URL = "https://enact.tools";
6
+ // Backend API URL - used for all API calls (search, publish, etc.)
7
+ export const DEFAULT_API_URL = "https://xjnhhxwxovjifdxdwzih.supabase.co";
8
+ // Environment variable names for overriding defaults
9
+ export const ENV_FRONTEND_URL = "ENACT_FRONTEND_URL";
10
+ export const ENV_API_URL = "ENACT_API_URL";
@@ -145,10 +145,6 @@ export declare class DaggerExecutionProvider extends ExecutionProvider {
145
145
  * Graceful shutdown with proper async cleanup
146
146
  */
147
147
  private gracefulShutdown;
148
- /**
149
- * Enhanced force cleanup for synchronous exit handlers
150
- */
151
- private forceCleanup;
152
148
  /**
153
149
  * Get current engine status for debugging
154
150
  */
@@ -935,43 +935,6 @@ export class DaggerExecutionProvider extends ExecutionProvider {
935
935
  process.exit(1);
936
936
  }
937
937
  }
938
- /**
939
- * Enhanced force cleanup for synchronous exit handlers
940
- */
941
- forceCleanup() {
942
- if (this.isShuttingDown)
943
- return;
944
- try {
945
- logger.info("🔄 Force cleaning up Dagger engines...");
946
- const result = spawnSync("docker", [
947
- "ps",
948
- "--all",
949
- "--filter",
950
- "name=dagger-engine",
951
- "--format",
952
- "{{.Names}}",
953
- ], {
954
- encoding: "utf8",
955
- timeout: 5000,
956
- });
957
- if (result.stdout) {
958
- const names = result.stdout
959
- .trim()
960
- .split("\n")
961
- .filter((n) => n.trim());
962
- if (names.length > 0) {
963
- logger.info(`Found ${names.length} engine containers, force removing...`);
964
- for (const name of names) {
965
- spawnSync("docker", ["rm", "-f", name.trim()], { timeout: 3000 });
966
- }
967
- logger.info("✅ Force cleanup completed");
968
- }
969
- }
970
- }
971
- catch (error) {
972
- logger.debug("Force cleanup failed (this is usually fine):", error);
973
- }
974
- }
975
938
  /**
976
939
  * Get current engine status for debugging
977
940
  */
@@ -34,6 +34,10 @@ export declare class EnactCore {
34
34
  private executionProvider;
35
35
  private options;
36
36
  constructor(options?: EnactCoreOptions);
37
+ /**
38
+ * Create EnactCore with config-based URLs
39
+ */
40
+ static create(options?: EnactCoreOptions): Promise<EnactCore>;
37
41
  /**
38
42
  * Set authentication token for API operations
39
43
  */
@@ -6,11 +6,12 @@ import { resolveToolEnvironmentVariables } from "../utils/env-loader.js";
6
6
  import logger from "../exec/logger.js";
7
7
  import yaml from "yaml";
8
8
  import { CryptoUtils, SecurityConfigManager, SigningService } from "@enactprotocol/security";
9
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
9
10
  export class EnactCore {
10
11
  constructor(options = {}) {
11
12
  this.options = {
12
- apiUrl: "https://enact.tools",
13
- supabaseUrl: "https://xjnhhxwxovjifdxdwzih.supabase.co",
13
+ apiUrl: "https://enact.tools", // Default, will be overridden by factory
14
+ supabaseUrl: "https://xjnhhxwxovjifdxdwzih.supabase.co", // Default, will be overridden by factory
14
15
  executionProvider: "dagger",
15
16
  defaultTimeout: "30s",
16
17
  ...options,
@@ -19,6 +20,18 @@ export class EnactCore {
19
20
  // Initialize the appropriate execution provider
20
21
  this.executionProvider = this.createExecutionProvider();
21
22
  }
23
+ /**
24
+ * Create EnactCore with config-based URLs
25
+ */
26
+ static async create(options = {}) {
27
+ const frontendUrl = options.apiUrl || await getFrontendUrl();
28
+ const apiUrl = options.supabaseUrl || await getApiUrl();
29
+ return new EnactCore({
30
+ ...options,
31
+ apiUrl: frontendUrl,
32
+ supabaseUrl: apiUrl,
33
+ });
34
+ }
22
35
  /**
23
36
  * Set authentication token for API operations
24
37
  */
@@ -14,6 +14,15 @@ export declare class EnactDirect {
14
14
  authToken?: string;
15
15
  defaultTimeout?: string;
16
16
  });
17
+ /**
18
+ * Create EnactDirect with config-based URLs
19
+ */
20
+ static create(options?: {
21
+ apiUrl?: string;
22
+ supabaseUrl?: string;
23
+ authToken?: string;
24
+ defaultTimeout?: string;
25
+ }): Promise<EnactDirect>;
17
26
  /**
18
27
  * Execute a tool by name with inputs
19
28
  *
@@ -1,5 +1,6 @@
1
1
  // src/lib/enact-direct.ts - Library interface for direct usage by MCP servers
2
2
  import { EnactCore, } from "../core/EnactCore";
3
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
3
4
  /**
4
5
  * Direct Enact Library Interface
5
6
  *
@@ -8,16 +9,27 @@ import { EnactCore, } from "../core/EnactCore";
8
9
  */
9
10
  export class EnactDirect {
10
11
  constructor(options = {}) {
12
+ // We need to handle async config loading in a factory method
11
13
  this.core = new EnactCore({
12
- apiUrl: options.apiUrl || process.env.ENACT_API_URL || "https://enact.tools",
13
- supabaseUrl: options.supabaseUrl ||
14
- process.env.ENACT_SUPABASE_URL ||
15
- "https://xjnhhxwxovjifdxdwzih.supabase.co",
14
+ apiUrl: options.apiUrl || process.env.ENACT_FRONTEND_URL || "https://enact.tools",
15
+ supabaseUrl: options.supabaseUrl || process.env.ENACT_API_URL || "https://xjnhhxwxovjifdxdwzih.supabase.co",
16
16
  executionProvider: "direct",
17
17
  authToken: options.authToken || process.env.ENACT_AUTH_TOKEN,
18
18
  defaultTimeout: options.defaultTimeout || "30s",
19
19
  });
20
20
  }
21
+ /**
22
+ * Create EnactDirect with config-based URLs
23
+ */
24
+ static async create(options = {}) {
25
+ const frontendUrl = options.apiUrl || process.env.ENACT_FRONTEND_URL || await getFrontendUrl();
26
+ const apiUrl = options.supabaseUrl || process.env.ENACT_API_URL || await getApiUrl();
27
+ return new EnactDirect({
28
+ ...options,
29
+ apiUrl: frontendUrl,
30
+ supabaseUrl: apiUrl,
31
+ });
32
+ }
21
33
  /**
22
34
  * Execute a tool by name with inputs
23
35
  *
@@ -6,6 +6,14 @@ export declare class McpCoreService {
6
6
  supabaseUrl?: string;
7
7
  authToken?: string;
8
8
  });
9
+ /**
10
+ * Create McpCoreService with config-based URLs
11
+ */
12
+ static create(options?: {
13
+ apiUrl?: string;
14
+ supabaseUrl?: string;
15
+ authToken?: string;
16
+ }): Promise<McpCoreService>;
9
17
  /**
10
18
  * Set authentication token
11
19
  */
@@ -1,5 +1,6 @@
1
1
  // src/services/McpCoreService.ts - Direct core integration for MCP server
2
2
  import { EnactCore } from "../core/EnactCore";
3
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
3
4
  export class McpCoreService {
4
5
  constructor(options) {
5
6
  this.core = new EnactCore({
@@ -8,6 +9,18 @@ export class McpCoreService {
8
9
  authToken: options?.authToken,
9
10
  });
10
11
  }
12
+ /**
13
+ * Create McpCoreService with config-based URLs
14
+ */
15
+ static async create(options) {
16
+ const frontendUrl = options?.apiUrl || await getFrontendUrl();
17
+ const apiUrl = options?.supabaseUrl || await getApiUrl();
18
+ return new McpCoreService({
19
+ ...options,
20
+ apiUrl: frontendUrl,
21
+ supabaseUrl: apiUrl,
22
+ });
23
+ }
11
24
  /**
12
25
  * Set authentication token
13
26
  */
@@ -1,6 +1,10 @@
1
1
  export interface EnactConfig {
2
2
  defaultUrl?: string;
3
3
  history?: string[];
4
+ urls?: {
5
+ frontend?: string;
6
+ api?: string;
7
+ };
4
8
  }
5
9
  /**
6
10
  * Ensure config directory and file exist
@@ -30,3 +34,78 @@ export declare function setDefaultUrl(url: string): Promise<void>;
30
34
  * Get the default publish URL
31
35
  */
32
36
  export declare function getDefaultUrl(): Promise<string | undefined>;
37
+ export interface TrustedKeyMeta {
38
+ name: string;
39
+ description?: string;
40
+ addedAt: string;
41
+ source: "default" | "user" | "organization";
42
+ keyFile: string;
43
+ }
44
+ export interface TrustedKey {
45
+ id: string;
46
+ name: string;
47
+ publicKey: string;
48
+ description?: string;
49
+ addedAt: string;
50
+ source: "default" | "user" | "organization";
51
+ keyFile: string;
52
+ }
53
+ /**
54
+ * Get the frontend URL with fallbacks
55
+ */
56
+ export declare function getFrontendUrl(): Promise<string>;
57
+ /**
58
+ * Get the API URL with fallbacks
59
+ */
60
+ export declare function getApiUrl(): Promise<string>;
61
+ /**
62
+ * Set the frontend URL in config
63
+ */
64
+ export declare function setFrontendUrl(url: string): Promise<void>;
65
+ /**
66
+ * Set the API URL in config
67
+ */
68
+ export declare function setApiUrl(url: string): Promise<void>;
69
+ /**
70
+ * Reset URLs to defaults
71
+ */
72
+ export declare function resetUrls(): Promise<void>;
73
+ /**
74
+ * Get current URL configuration
75
+ */
76
+ export declare function getUrlConfig(): Promise<{
77
+ frontend: {
78
+ value: string;
79
+ source: string;
80
+ };
81
+ api: {
82
+ value: string;
83
+ source: string;
84
+ };
85
+ }>;
86
+ /**
87
+ * Read all trusted keys from directory
88
+ */
89
+ export declare function getTrustedKeys(): Promise<TrustedKey[]>;
90
+ /**
91
+ * Add a trusted key
92
+ */
93
+ export declare function addTrustedKey(keyData: {
94
+ id: string;
95
+ name: string;
96
+ publicKey: string;
97
+ description?: string;
98
+ source?: "user" | "organization";
99
+ }): Promise<void>;
100
+ /**
101
+ * Remove a trusted key
102
+ */
103
+ export declare function removeTrustedKey(keyId: string): Promise<void>;
104
+ /**
105
+ * Get a specific trusted key
106
+ */
107
+ export declare function getTrustedKey(keyId: string): Promise<TrustedKey | null>;
108
+ /**
109
+ * Check if a public key is trusted
110
+ */
111
+ export declare function isKeyTrusted(publicKey: string): Promise<boolean>;
@@ -6,6 +6,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  // Define config paths
7
7
  const CONFIG_DIR = join(homedir(), ".enact");
8
8
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
9
+ const TRUSTED_KEYS_DIR = join(CONFIG_DIR, "trusted-keys");
9
10
  /**
10
11
  * Ensure config directory and file exist
11
12
  */
@@ -14,7 +15,14 @@ export async function ensureConfig() {
14
15
  await mkdir(CONFIG_DIR, { recursive: true });
15
16
  }
16
17
  if (!existsSync(CONFIG_FILE)) {
17
- await writeFile(CONFIG_FILE, JSON.stringify({ history: [] }, null, 2));
18
+ const defaultConfig = {
19
+ history: [],
20
+ urls: {
21
+ frontend: DEFAULT_FRONTEND_URL,
22
+ api: DEFAULT_API_URL,
23
+ },
24
+ };
25
+ await writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2));
18
26
  }
19
27
  }
20
28
  /**
@@ -24,11 +32,26 @@ export async function readConfig() {
24
32
  await ensureConfig();
25
33
  try {
26
34
  const data = await readFile(CONFIG_FILE, "utf8");
27
- return JSON.parse(data);
35
+ const config = JSON.parse(data);
36
+ // Migrate old configs that don't have URLs section
37
+ if (!config.urls) {
38
+ config.urls = {
39
+ frontend: DEFAULT_FRONTEND_URL,
40
+ api: DEFAULT_API_URL,
41
+ };
42
+ await writeConfig(config);
43
+ }
44
+ return config;
28
45
  }
29
46
  catch (error) {
30
47
  console.error("Failed to read config:", error.message);
31
- return { history: [] };
48
+ return {
49
+ history: [],
50
+ urls: {
51
+ frontend: DEFAULT_FRONTEND_URL,
52
+ api: DEFAULT_API_URL,
53
+ },
54
+ };
32
55
  }
33
56
  }
34
57
  /**
@@ -76,3 +99,244 @@ export async function getDefaultUrl() {
76
99
  const config = await readConfig();
77
100
  return config.defaultUrl;
78
101
  }
102
+ // Default URLs
103
+ const DEFAULT_FRONTEND_URL = "https://enact.tools";
104
+ const DEFAULT_API_URL = "https://xjnhhxwxovjifdxdwzih.supabase.co";
105
+ // Default trusted public key (Enact Protocol official key)
106
+ const DEFAULT_ENACT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
107
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8VyE3jGm5yT2mKnPx1dQF7q8Z2Kv
108
+ 7mX9YnE2mK8vF3tY9pL6xH2dF8sK3mN7wQ5vT2gR8sL4xN6pM9uE3wF2Qw==
109
+ -----END PUBLIC KEY-----`;
110
+ /**
111
+ * Get the frontend URL with fallbacks
112
+ */
113
+ export async function getFrontendUrl() {
114
+ // 1. Environment variable override
115
+ if (process.env.ENACT_FRONTEND_URL) {
116
+ return process.env.ENACT_FRONTEND_URL;
117
+ }
118
+ // 2. Config file setting
119
+ const config = await readConfig();
120
+ if (config.urls?.frontend) {
121
+ return config.urls.frontend;
122
+ }
123
+ // 3. Default
124
+ return DEFAULT_FRONTEND_URL;
125
+ }
126
+ /**
127
+ * Get the API URL with fallbacks
128
+ */
129
+ export async function getApiUrl() {
130
+ // 1. Environment variable override
131
+ if (process.env.ENACT_API_URL) {
132
+ return process.env.ENACT_API_URL;
133
+ }
134
+ // 2. Config file setting
135
+ const config = await readConfig();
136
+ if (config.urls?.api) {
137
+ return config.urls.api;
138
+ }
139
+ // 3. Default
140
+ return DEFAULT_API_URL;
141
+ }
142
+ /**
143
+ * Set the frontend URL in config
144
+ */
145
+ export async function setFrontendUrl(url) {
146
+ const config = await readConfig();
147
+ if (!config.urls) {
148
+ config.urls = {};
149
+ }
150
+ config.urls.frontend = url;
151
+ await writeConfig(config);
152
+ }
153
+ /**
154
+ * Set the API URL in config
155
+ */
156
+ export async function setApiUrl(url) {
157
+ const config = await readConfig();
158
+ if (!config.urls) {
159
+ config.urls = {};
160
+ }
161
+ config.urls.api = url;
162
+ await writeConfig(config);
163
+ }
164
+ /**
165
+ * Reset URLs to defaults
166
+ */
167
+ export async function resetUrls() {
168
+ const config = await readConfig();
169
+ if (config.urls) {
170
+ delete config.urls.frontend;
171
+ delete config.urls.api;
172
+ }
173
+ await writeConfig(config);
174
+ }
175
+ /**
176
+ * Get current URL configuration
177
+ */
178
+ export async function getUrlConfig() {
179
+ const config = await readConfig();
180
+ // Determine frontend URL source
181
+ let frontendValue = DEFAULT_FRONTEND_URL;
182
+ let frontendSource = "default";
183
+ if (config.urls?.frontend) {
184
+ frontendValue = config.urls.frontend;
185
+ frontendSource = "config";
186
+ }
187
+ if (process.env.ENACT_FRONTEND_URL) {
188
+ frontendValue = process.env.ENACT_FRONTEND_URL;
189
+ frontendSource = "environment";
190
+ }
191
+ // Determine API URL source
192
+ let apiValue = DEFAULT_API_URL;
193
+ let apiSource = "default";
194
+ if (config.urls?.api) {
195
+ apiValue = config.urls.api;
196
+ apiSource = "config";
197
+ }
198
+ if (process.env.ENACT_API_URL) {
199
+ apiValue = process.env.ENACT_API_URL;
200
+ apiSource = "environment";
201
+ }
202
+ return {
203
+ frontend: { value: frontendValue, source: frontendSource },
204
+ api: { value: apiValue, source: apiSource },
205
+ };
206
+ }
207
+ /**
208
+ * Ensure trusted keys directory exists with default key
209
+ */
210
+ async function ensureTrustedKeysDir() {
211
+ if (!existsSync(CONFIG_DIR)) {
212
+ await mkdir(CONFIG_DIR, { recursive: true });
213
+ }
214
+ if (!existsSync(TRUSTED_KEYS_DIR)) {
215
+ await mkdir(TRUSTED_KEYS_DIR, { recursive: true });
216
+ }
217
+ // Create default Enact Protocol key if it doesn't exist
218
+ const defaultKeyFile = join(TRUSTED_KEYS_DIR, "enact-protocol-official.pem");
219
+ const defaultMetaFile = join(TRUSTED_KEYS_DIR, "enact-protocol-official.meta");
220
+ if (!existsSync(defaultKeyFile)) {
221
+ await writeFile(defaultKeyFile, DEFAULT_ENACT_PUBLIC_KEY);
222
+ }
223
+ if (!existsSync(defaultMetaFile)) {
224
+ const defaultMeta = {
225
+ name: "Enact Protocol Official",
226
+ description: "Official Enact Protocol signing key for verified tools",
227
+ addedAt: new Date().toISOString(),
228
+ source: "default",
229
+ keyFile: "enact-protocol-official.pem"
230
+ };
231
+ await writeFile(defaultMetaFile, JSON.stringify(defaultMeta, null, 2));
232
+ }
233
+ }
234
+ /**
235
+ * Read all trusted keys from directory
236
+ */
237
+ export async function getTrustedKeys() {
238
+ await ensureTrustedKeysDir();
239
+ const keys = [];
240
+ try {
241
+ const { readdir } = await import('fs/promises');
242
+ const files = await readdir(TRUSTED_KEYS_DIR);
243
+ // Get all .pem files
244
+ const pemFiles = files.filter(f => f.endsWith('.pem'));
245
+ for (const pemFile of pemFiles) {
246
+ try {
247
+ const keyId = pemFile.replace('.pem', '');
248
+ const keyPath = join(TRUSTED_KEYS_DIR, pemFile);
249
+ const metaPath = join(TRUSTED_KEYS_DIR, `${keyId}.meta`);
250
+ // Read the public key
251
+ const publicKey = await readFile(keyPath, 'utf8');
252
+ // Read metadata if it exists
253
+ let meta = {
254
+ name: keyId,
255
+ addedAt: new Date().toISOString(),
256
+ source: "user",
257
+ keyFile: pemFile
258
+ };
259
+ if (existsSync(metaPath)) {
260
+ try {
261
+ const metaData = await readFile(metaPath, 'utf8');
262
+ meta = { ...meta, ...JSON.parse(metaData) };
263
+ }
264
+ catch {
265
+ // Use defaults if meta file is corrupted
266
+ }
267
+ }
268
+ keys.push({
269
+ id: keyId,
270
+ name: meta.name,
271
+ publicKey: publicKey.trim(),
272
+ description: meta.description,
273
+ addedAt: meta.addedAt,
274
+ source: meta.source,
275
+ keyFile: pemFile
276
+ });
277
+ }
278
+ catch (error) {
279
+ console.warn(`Warning: Could not read key file ${pemFile}:`, error);
280
+ }
281
+ }
282
+ }
283
+ catch (error) {
284
+ console.error("Failed to read trusted keys directory:", error);
285
+ }
286
+ return keys;
287
+ }
288
+ /**
289
+ * Add a trusted key
290
+ */
291
+ export async function addTrustedKey(keyData) {
292
+ await ensureTrustedKeysDir();
293
+ const keyFile = `${keyData.id}.pem`;
294
+ const metaFile = `${keyData.id}.meta`;
295
+ const keyPath = join(TRUSTED_KEYS_DIR, keyFile);
296
+ const metaPath = join(TRUSTED_KEYS_DIR, metaFile);
297
+ // Check if key already exists
298
+ if (existsSync(keyPath)) {
299
+ throw new Error(`Key with ID '${keyData.id}' already exists`);
300
+ }
301
+ // Write the public key file
302
+ await writeFile(keyPath, keyData.publicKey);
303
+ // Write the metadata file
304
+ const meta = {
305
+ name: keyData.name,
306
+ description: keyData.description,
307
+ addedAt: new Date().toISOString(),
308
+ source: keyData.source || "user",
309
+ keyFile
310
+ };
311
+ await writeFile(metaPath, JSON.stringify(meta, null, 2));
312
+ }
313
+ /**
314
+ * Remove a trusted key
315
+ */
316
+ export async function removeTrustedKey(keyId) {
317
+ const keyPath = join(TRUSTED_KEYS_DIR, `${keyId}.pem`);
318
+ const metaPath = join(TRUSTED_KEYS_DIR, `${keyId}.meta`);
319
+ if (!existsSync(keyPath)) {
320
+ throw new Error(`Trusted key '${keyId}' not found`);
321
+ }
322
+ // Remove both files
323
+ const { unlink } = await import('fs/promises');
324
+ await unlink(keyPath);
325
+ if (existsSync(metaPath)) {
326
+ await unlink(metaPath);
327
+ }
328
+ }
329
+ /**
330
+ * Get a specific trusted key
331
+ */
332
+ export async function getTrustedKey(keyId) {
333
+ const keys = await getTrustedKeys();
334
+ return keys.find(k => k.id === keyId) || null;
335
+ }
336
+ /**
337
+ * Check if a public key is trusted
338
+ */
339
+ export async function isKeyTrusted(publicKey) {
340
+ const keys = await getTrustedKeys();
341
+ return keys.some(k => k.publicKey.trim() === publicKey.trim());
342
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enactprotocol/shared",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Shared utilities and core functionality for Enact Protocol",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,19 +5,32 @@ import {
5
5
  CLITokenCreate,
6
6
  OAuthTokenExchange,
7
7
  } from "./types";
8
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
8
9
 
9
10
  export class EnactApiClient {
10
11
  baseUrl: string;
11
12
  supabaseUrl: string;
12
13
 
13
14
  constructor(
14
- baseUrl: string = "https://enact.tools",
15
- supabaseUrl: string = "https://xjnhhxwxovjifdxdwzih.supabase.co",
15
+ baseUrl: string,
16
+ supabaseUrl: string,
16
17
  ) {
17
18
  this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash
18
19
  this.supabaseUrl = supabaseUrl.replace(/\/$/, "");
19
20
  }
20
21
 
22
+ /**
23
+ * Create API client with config-based URLs
24
+ */
25
+ static async create(
26
+ baseUrl?: string,
27
+ supabaseUrl?: string,
28
+ ): Promise<EnactApiClient> {
29
+ const frontendUrl = baseUrl || await getFrontendUrl();
30
+ const apiUrl = supabaseUrl || await getApiUrl();
31
+ return new EnactApiClient(frontendUrl, apiUrl);
32
+ }
33
+
21
34
  // Helper method to make authenticated requests
22
35
  private async makeRequest<T>(
23
36
  endpoint: string,
@@ -545,8 +558,13 @@ export class EnactApiClient {
545
558
  }
546
559
  }
547
560
 
548
- // Export a default instance
549
- export const enactApi = new EnactApiClient();
561
+ // Export a default instance factory
562
+ export async function createDefaultApiClient(): Promise<EnactApiClient> {
563
+ return await EnactApiClient.create();
564
+ }
565
+
566
+ // Keep backward compatibility with sync usage
567
+ export const enactApi = new EnactApiClient("https://enact.tools", "https://xjnhhxwxovjifdxdwzih.supabase.co");
550
568
 
551
569
  // Export error types for better error handling
552
570
  export class EnactApiError extends Error {
@@ -565,5 +583,7 @@ export function createEnactApiClient(
565
583
  baseUrl?: string,
566
584
  supabaseUrl?: string,
567
585
  ): EnactApiClient {
568
- return new EnactApiClient(baseUrl, supabaseUrl);
586
+ const defaultFrontend = "https://enact.tools";
587
+ const defaultApi = "https://xjnhhxwxovjifdxdwzih.supabase.co";
588
+ return new EnactApiClient(baseUrl || defaultFrontend, supabaseUrl || defaultApi);
569
589
  }
@@ -12,6 +12,7 @@ import fs from "fs/promises";
12
12
  import path from "path";
13
13
  import crypto from "crypto";
14
14
  import { spawn, spawnSync } from "child_process";
15
+ import { exit } from "process";
15
16
 
16
17
  export interface DaggerExecutionOptions {
17
18
  baseImage?: string; // Default container image
@@ -457,7 +458,8 @@ export class DaggerExecutionProvider extends ExecutionProvider {
457
458
  logger.debug(`Waiting ${waitTime}ms before retry...`);
458
459
  await new Promise((resolve) => setTimeout(resolve, waitTime));
459
460
  }
460
- }
461
+ }
462
+
461
463
  }
462
464
 
463
465
  // All retries failed
@@ -623,6 +625,7 @@ export class DaggerExecutionProvider extends ExecutionProvider {
623
625
  throw error;
624
626
  } finally {
625
627
  this.abortController = null;
628
+
626
629
  }
627
630
  }
628
631
 
@@ -1240,50 +1243,7 @@ export class DaggerExecutionProvider extends ExecutionProvider {
1240
1243
  }
1241
1244
  }
1242
1245
 
1243
- /**
1244
- * Enhanced force cleanup for synchronous exit handlers
1245
- */
1246
- private forceCleanup(): void {
1247
- if (this.isShuttingDown) return;
1248
-
1249
- try {
1250
- logger.info("🔄 Force cleaning up Dagger engines...");
1251
-
1252
- const result = spawnSync(
1253
- "docker",
1254
- [
1255
- "ps",
1256
- "--all",
1257
- "--filter",
1258
- "name=dagger-engine",
1259
- "--format",
1260
- "{{.Names}}",
1261
- ],
1262
- {
1263
- encoding: "utf8",
1264
- timeout: 5000,
1265
- },
1266
- );
1267
-
1268
- if (result.stdout) {
1269
- const names = result.stdout
1270
- .trim()
1271
- .split("\n")
1272
- .filter((n: string) => n.trim());
1273
- if (names.length > 0) {
1274
- logger.info(
1275
- `Found ${names.length} engine containers, force removing...`,
1276
- );
1277
- for (const name of names) {
1278
- spawnSync("docker", ["rm", "-f", name.trim()], { timeout: 3000 });
1279
- }
1280
- logger.info("✅ Force cleanup completed");
1281
- }
1282
- }
1283
- } catch (error) {
1284
- logger.debug("Force cleanup failed (this is usually fine):", error);
1285
- }
1286
- }
1246
+
1287
1247
 
1288
1248
  /**
1289
1249
  * Get current engine status for debugging
@@ -18,6 +18,7 @@ import yaml from "yaml";
18
18
  import fs from "fs";
19
19
  import path from "path";
20
20
  import { CryptoUtils, KeyManager, SecurityConfigManager, SigningService } from "@enactprotocol/security";
21
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
21
22
 
22
23
  export interface EnactCoreOptions {
23
24
  apiUrl?: string;
@@ -60,21 +61,35 @@ export class EnactCore {
60
61
 
61
62
  constructor(options: EnactCoreOptions = {}) {
62
63
  this.options = {
63
- apiUrl: "https://enact.tools",
64
- supabaseUrl: "https://xjnhhxwxovjifdxdwzih.supabase.co",
64
+ apiUrl: "https://enact.tools", // Default, will be overridden by factory
65
+ supabaseUrl: "https://xjnhhxwxovjifdxdwzih.supabase.co", // Default, will be overridden by factory
65
66
  executionProvider: "dagger",
66
67
  defaultTimeout: "30s",
67
68
  ...options,
68
69
  };
69
70
 
70
71
  this.apiClient = new EnactApiClient(
71
- this.options.apiUrl,
72
- this.options.supabaseUrl,
72
+ this.options.apiUrl!,
73
+ this.options.supabaseUrl!,
73
74
  );
74
75
 
75
76
  // Initialize the appropriate execution provider
76
77
  this.executionProvider = this.createExecutionProvider();
77
78
  }
79
+
80
+ /**
81
+ * Create EnactCore with config-based URLs
82
+ */
83
+ static async create(options: EnactCoreOptions = {}): Promise<EnactCore> {
84
+ const frontendUrl = options.apiUrl || await getFrontendUrl();
85
+ const apiUrl = options.supabaseUrl || await getApiUrl();
86
+
87
+ return new EnactCore({
88
+ ...options,
89
+ apiUrl: frontendUrl,
90
+ supabaseUrl: apiUrl,
91
+ });
92
+ }
78
93
  /**
79
94
  * Set authentication token for API operations
80
95
  */
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export { EnactCore } from './core/EnactCore';
3
3
  export { DirectExecutionProvider } from './core/DirectExecutionProvider';
4
4
  export { DaggerExecutionProvider } from './core/DaggerExecutionProvider';
5
5
 
6
+ // Constants - now handled in config utils
7
+
6
8
  // Types and utilities
7
9
  export type { EnactTool } from './types';
8
10
  export type { EnactToolDefinition } from './api/types';
@@ -5,6 +5,7 @@ import {
5
5
  type ToolExecuteOptions,
6
6
  } from "../core/EnactCore";
7
7
  import type { EnactTool, ExecutionResult } from "../types";
8
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
8
9
 
9
10
  /**
10
11
  * Direct Enact Library Interface
@@ -23,19 +24,35 @@ export class EnactDirect {
23
24
  defaultTimeout?: string;
24
25
  } = {},
25
26
  ) {
27
+ // We need to handle async config loading in a factory method
26
28
  this.core = new EnactCore({
27
- apiUrl:
28
- options.apiUrl || process.env.ENACT_API_URL || "https://enact.tools",
29
- supabaseUrl:
30
- options.supabaseUrl ||
31
- process.env.ENACT_SUPABASE_URL ||
32
- "https://xjnhhxwxovjifdxdwzih.supabase.co",
29
+ apiUrl: options.apiUrl || process.env.ENACT_FRONTEND_URL || "https://enact.tools",
30
+ supabaseUrl: options.supabaseUrl || process.env.ENACT_API_URL || "https://xjnhhxwxovjifdxdwzih.supabase.co",
33
31
  executionProvider: "direct",
34
32
  authToken: options.authToken || process.env.ENACT_AUTH_TOKEN,
35
33
  defaultTimeout: options.defaultTimeout || "30s",
36
34
  });
37
35
  }
38
36
 
37
+ /**
38
+ * Create EnactDirect with config-based URLs
39
+ */
40
+ static async create(options: {
41
+ apiUrl?: string;
42
+ supabaseUrl?: string;
43
+ authToken?: string;
44
+ defaultTimeout?: string;
45
+ } = {}): Promise<EnactDirect> {
46
+ const frontendUrl = options.apiUrl || process.env.ENACT_FRONTEND_URL || await getFrontendUrl();
47
+ const apiUrl = options.supabaseUrl || process.env.ENACT_API_URL || await getApiUrl();
48
+
49
+ return new EnactDirect({
50
+ ...options,
51
+ apiUrl: frontendUrl,
52
+ supabaseUrl: apiUrl,
53
+ });
54
+ }
55
+
39
56
  /**
40
57
  * Execute a tool by name with inputs
41
58
  *
@@ -2,6 +2,7 @@
2
2
  import { EnactCore } from "../core/EnactCore";
3
3
  import type { EnactTool, ExecutionResult } from "../types";
4
4
  import type { ToolSearchOptions, ToolExecuteOptions } from "../core/EnactCore";
5
+ import { getFrontendUrl, getApiUrl } from "../utils/config";
5
6
 
6
7
  export class McpCoreService {
7
8
  private core: EnactCore;
@@ -13,12 +14,29 @@ export class McpCoreService {
13
14
  }) {
14
15
  this.core = new EnactCore({
15
16
  apiUrl: options?.apiUrl || "https://enact.tools",
16
- supabaseUrl:
17
- options?.supabaseUrl || "https://xjnhhxwxovjifdxdwzih.supabase.co",
17
+ supabaseUrl: options?.supabaseUrl || "https://xjnhhxwxovjifdxdwzih.supabase.co",
18
18
  authToken: options?.authToken,
19
19
  });
20
20
  }
21
21
 
22
+ /**
23
+ * Create McpCoreService with config-based URLs
24
+ */
25
+ static async create(options?: {
26
+ apiUrl?: string;
27
+ supabaseUrl?: string;
28
+ authToken?: string;
29
+ }): Promise<McpCoreService> {
30
+ const frontendUrl = options?.apiUrl || await getFrontendUrl();
31
+ const apiUrl = options?.supabaseUrl || await getApiUrl();
32
+
33
+ return new McpCoreService({
34
+ ...options,
35
+ apiUrl: frontendUrl,
36
+ supabaseUrl: apiUrl,
37
+ });
38
+ }
39
+
22
40
  /**
23
41
  * Set authentication token
24
42
  */
@@ -7,11 +7,16 @@ import { mkdir, readFile, writeFile } from "fs/promises";
7
7
  // Define config paths
8
8
  const CONFIG_DIR = join(homedir(), ".enact");
9
9
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
10
+ const TRUSTED_KEYS_DIR = join(CONFIG_DIR, "trusted-keys");
10
11
 
11
12
  // Define config interface
12
13
  export interface EnactConfig {
13
14
  defaultUrl?: string;
14
15
  history?: string[];
16
+ urls?: {
17
+ frontend?: string;
18
+ api?: string;
19
+ };
15
20
  }
16
21
 
17
22
  /**
@@ -23,7 +28,14 @@ export async function ensureConfig(): Promise<void> {
23
28
  }
24
29
 
25
30
  if (!existsSync(CONFIG_FILE)) {
26
- await writeFile(CONFIG_FILE, JSON.stringify({ history: [] }, null, 2));
31
+ const defaultConfig = {
32
+ history: [],
33
+ urls: {
34
+ frontend: DEFAULT_FRONTEND_URL,
35
+ api: DEFAULT_API_URL,
36
+ },
37
+ };
38
+ await writeFile(CONFIG_FILE, JSON.stringify(defaultConfig, null, 2));
27
39
  }
28
40
  }
29
41
 
@@ -35,10 +47,27 @@ export async function readConfig(): Promise<EnactConfig> {
35
47
 
36
48
  try {
37
49
  const data = await readFile(CONFIG_FILE, "utf8");
38
- return JSON.parse(data) as EnactConfig;
50
+ const config = JSON.parse(data) as EnactConfig;
51
+
52
+ // Migrate old configs that don't have URLs section
53
+ if (!config.urls) {
54
+ config.urls = {
55
+ frontend: DEFAULT_FRONTEND_URL,
56
+ api: DEFAULT_API_URL,
57
+ };
58
+ await writeConfig(config);
59
+ }
60
+
61
+ return config;
39
62
  } catch (error) {
40
63
  console.error("Failed to read config:", (error as Error).message);
41
- return { history: [] };
64
+ return {
65
+ history: [],
66
+ urls: {
67
+ frontend: DEFAULT_FRONTEND_URL,
68
+ api: DEFAULT_API_URL,
69
+ },
70
+ };
42
71
  }
43
72
  }
44
73
 
@@ -95,3 +124,315 @@ export async function getDefaultUrl(): Promise<string | undefined> {
95
124
  const config = await readConfig();
96
125
  return config.defaultUrl;
97
126
  }
127
+
128
+ // Default URLs
129
+ const DEFAULT_FRONTEND_URL = "https://enact.tools";
130
+ const DEFAULT_API_URL = "https://xjnhhxwxovjifdxdwzih.supabase.co";
131
+
132
+ // Default trusted public key (Enact Protocol official key)
133
+ const DEFAULT_ENACT_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
134
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8VyE3jGm5yT2mKnPx1dQF7q8Z2Kv
135
+ 7mX9YnE2mK8vF3tY9pL6xH2dF8sK3mN7wQ5vT2gR8sL4xN6pM9uE3wF2Qw==
136
+ -----END PUBLIC KEY-----`;
137
+
138
+ // Trusted key metadata interface (for .meta files)
139
+ export interface TrustedKeyMeta {
140
+ name: string;
141
+ description?: string;
142
+ addedAt: string;
143
+ source: "default" | "user" | "organization";
144
+ keyFile: string;
145
+ }
146
+
147
+ // Combined trusted key interface
148
+ export interface TrustedKey {
149
+ id: string;
150
+ name: string;
151
+ publicKey: string;
152
+ description?: string;
153
+ addedAt: string;
154
+ source: "default" | "user" | "organization";
155
+ keyFile: string;
156
+ }
157
+
158
+ /**
159
+ * Get the frontend URL with fallbacks
160
+ */
161
+ export async function getFrontendUrl(): Promise<string> {
162
+ // 1. Environment variable override
163
+ if (process.env.ENACT_FRONTEND_URL) {
164
+ return process.env.ENACT_FRONTEND_URL;
165
+ }
166
+
167
+ // 2. Config file setting
168
+ const config = await readConfig();
169
+ if (config.urls?.frontend) {
170
+ return config.urls.frontend;
171
+ }
172
+
173
+ // 3. Default
174
+ return DEFAULT_FRONTEND_URL;
175
+ }
176
+
177
+ /**
178
+ * Get the API URL with fallbacks
179
+ */
180
+ export async function getApiUrl(): Promise<string> {
181
+ // 1. Environment variable override
182
+ if (process.env.ENACT_API_URL) {
183
+ return process.env.ENACT_API_URL;
184
+ }
185
+
186
+ // 2. Config file setting
187
+ const config = await readConfig();
188
+ if (config.urls?.api) {
189
+ return config.urls.api;
190
+ }
191
+
192
+ // 3. Default
193
+ return DEFAULT_API_URL;
194
+ }
195
+
196
+ /**
197
+ * Set the frontend URL in config
198
+ */
199
+ export async function setFrontendUrl(url: string): Promise<void> {
200
+ const config = await readConfig();
201
+ if (!config.urls) {
202
+ config.urls = {};
203
+ }
204
+ config.urls.frontend = url;
205
+ await writeConfig(config);
206
+ }
207
+
208
+ /**
209
+ * Set the API URL in config
210
+ */
211
+ export async function setApiUrl(url: string): Promise<void> {
212
+ const config = await readConfig();
213
+ if (!config.urls) {
214
+ config.urls = {};
215
+ }
216
+ config.urls.api = url;
217
+ await writeConfig(config);
218
+ }
219
+
220
+ /**
221
+ * Reset URLs to defaults
222
+ */
223
+ export async function resetUrls(): Promise<void> {
224
+ const config = await readConfig();
225
+ if (config.urls) {
226
+ delete config.urls.frontend;
227
+ delete config.urls.api;
228
+ }
229
+ await writeConfig(config);
230
+ }
231
+
232
+ /**
233
+ * Get current URL configuration
234
+ */
235
+ export async function getUrlConfig(): Promise<{
236
+ frontend: { value: string; source: string };
237
+ api: { value: string; source: string };
238
+ }> {
239
+ const config = await readConfig();
240
+
241
+ // Determine frontend URL source
242
+ let frontendValue = DEFAULT_FRONTEND_URL;
243
+ let frontendSource = "default";
244
+
245
+ if (config.urls?.frontend) {
246
+ frontendValue = config.urls.frontend;
247
+ frontendSource = "config";
248
+ }
249
+
250
+ if (process.env.ENACT_FRONTEND_URL) {
251
+ frontendValue = process.env.ENACT_FRONTEND_URL;
252
+ frontendSource = "environment";
253
+ }
254
+
255
+ // Determine API URL source
256
+ let apiValue = DEFAULT_API_URL;
257
+ let apiSource = "default";
258
+
259
+ if (config.urls?.api) {
260
+ apiValue = config.urls.api;
261
+ apiSource = "config";
262
+ }
263
+
264
+ if (process.env.ENACT_API_URL) {
265
+ apiValue = process.env.ENACT_API_URL;
266
+ apiSource = "environment";
267
+ }
268
+
269
+ return {
270
+ frontend: { value: frontendValue, source: frontendSource },
271
+ api: { value: apiValue, source: apiSource },
272
+ };
273
+ }
274
+
275
+ /**
276
+ * Ensure trusted keys directory exists with default key
277
+ */
278
+ async function ensureTrustedKeysDir(): Promise<void> {
279
+ if (!existsSync(CONFIG_DIR)) {
280
+ await mkdir(CONFIG_DIR, { recursive: true });
281
+ }
282
+
283
+ if (!existsSync(TRUSTED_KEYS_DIR)) {
284
+ await mkdir(TRUSTED_KEYS_DIR, { recursive: true });
285
+ }
286
+
287
+ // Create default Enact Protocol key if it doesn't exist
288
+ const defaultKeyFile = join(TRUSTED_KEYS_DIR, "enact-protocol-official.pem");
289
+ const defaultMetaFile = join(TRUSTED_KEYS_DIR, "enact-protocol-official.meta");
290
+
291
+ if (!existsSync(defaultKeyFile)) {
292
+ await writeFile(defaultKeyFile, DEFAULT_ENACT_PUBLIC_KEY);
293
+ }
294
+
295
+ if (!existsSync(defaultMetaFile)) {
296
+ const defaultMeta: TrustedKeyMeta = {
297
+ name: "Enact Protocol Official",
298
+ description: "Official Enact Protocol signing key for verified tools",
299
+ addedAt: new Date().toISOString(),
300
+ source: "default",
301
+ keyFile: "enact-protocol-official.pem"
302
+ };
303
+ await writeFile(defaultMetaFile, JSON.stringify(defaultMeta, null, 2));
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Read all trusted keys from directory
309
+ */
310
+ export async function getTrustedKeys(): Promise<TrustedKey[]> {
311
+ await ensureTrustedKeysDir();
312
+
313
+ const keys: TrustedKey[] = [];
314
+
315
+ try {
316
+ const { readdir } = await import('fs/promises');
317
+ const files = await readdir(TRUSTED_KEYS_DIR);
318
+
319
+ // Get all .pem files
320
+ const pemFiles = files.filter(f => f.endsWith('.pem'));
321
+
322
+ for (const pemFile of pemFiles) {
323
+ try {
324
+ const keyId = pemFile.replace('.pem', '');
325
+ const keyPath = join(TRUSTED_KEYS_DIR, pemFile);
326
+ const metaPath = join(TRUSTED_KEYS_DIR, `${keyId}.meta`);
327
+
328
+ // Read the public key
329
+ const publicKey = await readFile(keyPath, 'utf8');
330
+
331
+ // Read metadata if it exists
332
+ let meta: TrustedKeyMeta = {
333
+ name: keyId,
334
+ addedAt: new Date().toISOString(),
335
+ source: "user",
336
+ keyFile: pemFile
337
+ };
338
+
339
+ if (existsSync(metaPath)) {
340
+ try {
341
+ const metaData = await readFile(metaPath, 'utf8');
342
+ meta = { ...meta, ...JSON.parse(metaData) };
343
+ } catch {
344
+ // Use defaults if meta file is corrupted
345
+ }
346
+ }
347
+
348
+ keys.push({
349
+ id: keyId,
350
+ name: meta.name,
351
+ publicKey: publicKey.trim(),
352
+ description: meta.description,
353
+ addedAt: meta.addedAt,
354
+ source: meta.source,
355
+ keyFile: pemFile
356
+ });
357
+ } catch (error) {
358
+ console.warn(`Warning: Could not read key file ${pemFile}:`, error);
359
+ }
360
+ }
361
+ } catch (error) {
362
+ console.error("Failed to read trusted keys directory:", error);
363
+ }
364
+
365
+ return keys;
366
+ }
367
+
368
+ /**
369
+ * Add a trusted key
370
+ */
371
+ export async function addTrustedKey(keyData: {
372
+ id: string;
373
+ name: string;
374
+ publicKey: string;
375
+ description?: string;
376
+ source?: "user" | "organization";
377
+ }): Promise<void> {
378
+ await ensureTrustedKeysDir();
379
+
380
+ const keyFile = `${keyData.id}.pem`;
381
+ const metaFile = `${keyData.id}.meta`;
382
+ const keyPath = join(TRUSTED_KEYS_DIR, keyFile);
383
+ const metaPath = join(TRUSTED_KEYS_DIR, metaFile);
384
+
385
+ // Check if key already exists
386
+ if (existsSync(keyPath)) {
387
+ throw new Error(`Key with ID '${keyData.id}' already exists`);
388
+ }
389
+
390
+ // Write the public key file
391
+ await writeFile(keyPath, keyData.publicKey);
392
+
393
+ // Write the metadata file
394
+ const meta: TrustedKeyMeta = {
395
+ name: keyData.name,
396
+ description: keyData.description,
397
+ addedAt: new Date().toISOString(),
398
+ source: keyData.source || "user",
399
+ keyFile
400
+ };
401
+ await writeFile(metaPath, JSON.stringify(meta, null, 2));
402
+ }
403
+
404
+ /**
405
+ * Remove a trusted key
406
+ */
407
+ export async function removeTrustedKey(keyId: string): Promise<void> {
408
+ const keyPath = join(TRUSTED_KEYS_DIR, `${keyId}.pem`);
409
+ const metaPath = join(TRUSTED_KEYS_DIR, `${keyId}.meta`);
410
+
411
+ if (!existsSync(keyPath)) {
412
+ throw new Error(`Trusted key '${keyId}' not found`);
413
+ }
414
+
415
+ // Remove both files
416
+ const { unlink } = await import('fs/promises');
417
+ await unlink(keyPath);
418
+
419
+ if (existsSync(metaPath)) {
420
+ await unlink(metaPath);
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Get a specific trusted key
426
+ */
427
+ export async function getTrustedKey(keyId: string): Promise<TrustedKey | null> {
428
+ const keys = await getTrustedKeys();
429
+ return keys.find(k => k.id === keyId) || null;
430
+ }
431
+
432
+ /**
433
+ * Check if a public key is trusted
434
+ */
435
+ export async function isKeyTrusted(publicKey: string): Promise<boolean> {
436
+ const keys = await getTrustedKeys();
437
+ return keys.some(k => k.publicKey.trim() === publicKey.trim());
438
+ }