@iterable/mcp 1.0.3 → 1.1.0
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.
- package/README.md +29 -29
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -5
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts +6 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +174 -157
- package/dist/install.js.map +1 -1
- package/dist/key-manager.d.ts +27 -33
- package/dist/key-manager.d.ts.map +1 -1
- package/dist/key-manager.js +157 -144
- package/dist/key-manager.js.map +1 -1
- package/dist/keys-cli.d.ts.map +1 -1
- package/dist/keys-cli.js +27 -18
- package/dist/keys-cli.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/dist/utils/formatting.d.ts +6 -1
- package/dist/utils/formatting.d.ts.map +1 -1
- package/dist/utils/formatting.js +13 -1
- package/dist/utils/formatting.js.map +1 -1
- package/dist/utils/ui.d.ts +1 -1
- package/dist/utils/ui.js +1 -1
- package/package.json +38 -30
package/dist/key-manager.d.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secure API Key Management
|
|
2
|
+
* Secure API Key Management with cross-platform support
|
|
3
3
|
*
|
|
4
|
-
* This module provides secure storage and management of multiple Iterable API keys
|
|
5
|
-
* using macOS Keychain via the native `security` command-line tool.
|
|
4
|
+
* This module provides secure storage and management of multiple Iterable API keys.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* - macOS:
|
|
9
|
-
* - Windows
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - API keys: Stored securely in macOS Keychain (never on disk)
|
|
13
|
-
* - Metadata: Stored in ~/.iterable-mcp/keys.json (names, IDs, endpoints, timestamps)
|
|
14
|
-
* - Lock file: ~/.iterable-mcp/keys.lock (prevents concurrent modifications)
|
|
6
|
+
* Storage Strategy:
|
|
7
|
+
* - macOS: API keys in Keychain, metadata in ~/.iterable-mcp/keys.json
|
|
8
|
+
* - Windows: API keys and metadata in ~/.iterable-mcp/keys.json
|
|
9
|
+
* - Linux: API keys and metadata in ~/.iterable-mcp/keys.json (mode 0o600)
|
|
10
|
+
* - Lock file: ~/.iterable-mcp/keys.lock prevents concurrent modifications
|
|
15
11
|
*
|
|
16
12
|
* Security Features:
|
|
17
|
-
* - Uses spawn() with argument arrays to prevent shell injection
|
|
18
|
-
* - File-based locking for concurrent access protection
|
|
19
|
-
* - Test-specific service name to isolate test keychain entries
|
|
20
13
|
* - API key format validation (32-char lowercase hex)
|
|
21
|
-
* - HTTPS-only URL validation
|
|
14
|
+
* - HTTPS-only URL validation (except localhost)
|
|
22
15
|
* - Duplicate key detection (both names and values)
|
|
16
|
+
* - File-based locking for concurrent access protection
|
|
17
|
+
* - Restrictive file permissions where supported (Linux/macOS)
|
|
23
18
|
*/
|
|
24
19
|
export type SecurityExecutor = (args: string[]) => Promise<string>;
|
|
25
20
|
export interface ApiKeyMetadata {
|
|
@@ -35,6 +30,8 @@ export interface ApiKeyMetadata {
|
|
|
35
30
|
isActive: boolean;
|
|
36
31
|
/** Optional per-key environment overrides (extensible for future vars) */
|
|
37
32
|
env?: Record<string, string>;
|
|
33
|
+
/** API key value (only present when not using Keychain storage) */
|
|
34
|
+
apiKey?: string;
|
|
38
35
|
}
|
|
39
36
|
export declare class KeyManager {
|
|
40
37
|
private readonly configDir;
|
|
@@ -43,16 +40,16 @@ export declare class KeyManager {
|
|
|
43
40
|
private store;
|
|
44
41
|
private saveLock;
|
|
45
42
|
private readonly execSecurity;
|
|
43
|
+
private readonly useKeychain;
|
|
46
44
|
/**
|
|
47
45
|
* Create a new KeyManager instance
|
|
48
46
|
*
|
|
49
47
|
* @param configDir - Optional custom config directory (defaults to ~/.iterable-mcp)
|
|
50
48
|
* @param execSecurity - Optional security command executor (for dependency injection in tests)
|
|
51
|
-
* @throws {Error} If not running on macOS and no mock executor is provided
|
|
52
49
|
*/
|
|
53
50
|
constructor(configDir?: string, execSecurity?: SecurityExecutor);
|
|
54
51
|
/**
|
|
55
|
-
* Validate metadata against keychain and clean up orphaned entries
|
|
52
|
+
* Validate metadata against keychain and clean up orphaned entries (macOS only)
|
|
56
53
|
*
|
|
57
54
|
* Checks if each key in metadata still exists in the keychain.
|
|
58
55
|
* Removes any metadata entries that no longer have corresponding keychain entries.
|
|
@@ -114,15 +111,13 @@ export declare class KeyManager {
|
|
|
114
111
|
/**
|
|
115
112
|
* Add a new API key
|
|
116
113
|
*
|
|
117
|
-
*
|
|
118
|
-
* (name, base URL, timestamps) to disk. Each key is tightly coupled to
|
|
119
|
-
* its API endpoint.
|
|
114
|
+
* Stores an API key securely (Keychain on macOS, file on other platforms).
|
|
120
115
|
*
|
|
121
116
|
* @param name - User-friendly name for the key (must be unique)
|
|
122
117
|
* @param apiKey - 32-character lowercase hexadecimal Iterable API key
|
|
123
118
|
* @param baseUrl - Iterable API base URL (must be HTTPS)
|
|
124
119
|
* @returns The unique ID generated for this key
|
|
125
|
-
* @throws {Error} If the key name already exists, validation fails, or
|
|
120
|
+
* @throws {Error} If the key name already exists, validation fails, or storage fails
|
|
126
121
|
*/
|
|
127
122
|
addKey(name: string, apiKey: string, baseUrl: string, envOverrides?: Record<string, string>): Promise<string>;
|
|
128
123
|
/**
|
|
@@ -142,11 +137,11 @@ export declare class KeyManager {
|
|
|
142
137
|
/**
|
|
143
138
|
* Get a key by ID or name
|
|
144
139
|
*
|
|
145
|
-
* Retrieves the actual API key value from
|
|
140
|
+
* Retrieves the actual API key value from storage.
|
|
146
141
|
*
|
|
147
142
|
* @param idOrName - The unique ID or user-friendly name of the key
|
|
148
143
|
* @returns The API key value, or null if not found
|
|
149
|
-
* @throws {Error} If
|
|
144
|
+
* @throws {Error} If storage access fails
|
|
150
145
|
*/
|
|
151
146
|
getKey(idOrName: string): Promise<string | null>;
|
|
152
147
|
/**
|
|
@@ -156,14 +151,14 @@ export declare class KeyManager {
|
|
|
156
151
|
* Only one key can be active at a time.
|
|
157
152
|
*
|
|
158
153
|
* @returns The active API key value, or null if no key is active
|
|
159
|
-
* @throws {Error} If
|
|
154
|
+
* @throws {Error} If storage access fails
|
|
160
155
|
*/
|
|
161
156
|
getActiveKey(): Promise<string | null>;
|
|
162
157
|
/**
|
|
163
158
|
* Get the active key metadata
|
|
164
159
|
*
|
|
165
160
|
* Returns metadata for the currently active key without retrieving the
|
|
166
|
-
* actual API key value
|
|
161
|
+
* actual API key value.
|
|
167
162
|
*
|
|
168
163
|
* @returns The active key metadata, or null if no key is active
|
|
169
164
|
* @throws {Error} If the key store is not initialized
|
|
@@ -183,10 +178,9 @@ export declare class KeyManager {
|
|
|
183
178
|
/**
|
|
184
179
|
* Delete a key by ID
|
|
185
180
|
*
|
|
186
|
-
* Removes a key from
|
|
181
|
+
* Removes a key from storage and the metadata store.
|
|
187
182
|
* The currently active key cannot be deleted - you must activate a different
|
|
188
|
-
* key first.
|
|
189
|
-
* and a warning will be displayed.
|
|
183
|
+
* key first.
|
|
190
184
|
*
|
|
191
185
|
* Note: Only accepts key ID (not name) since IDs are guaranteed unique.
|
|
192
186
|
*
|
|
@@ -206,12 +200,12 @@ export declare class KeyManager {
|
|
|
206
200
|
/**
|
|
207
201
|
* Find a key by its actual API key value
|
|
208
202
|
*
|
|
209
|
-
* Checks all stored keys
|
|
210
|
-
*
|
|
203
|
+
* Checks all stored keys to see if the given API key value already exists.
|
|
204
|
+
* Useful for preventing duplicate key values.
|
|
211
205
|
*
|
|
212
206
|
* @param apiKeyValue - The API key value to search for
|
|
213
207
|
* @returns The metadata of the matching key, or null if not found
|
|
214
|
-
* @throws {Error} If
|
|
208
|
+
* @throws {Error} If storage access fails
|
|
215
209
|
*/
|
|
216
210
|
findKeyByValue(apiKeyValue: string): Promise<ApiKeyMetadata | null>;
|
|
217
211
|
/**
|
|
@@ -219,13 +213,13 @@ export declare class KeyManager {
|
|
|
219
213
|
*
|
|
220
214
|
* Adds an API key to the key manager with a default name if it doesn't
|
|
221
215
|
* already exist. This is used during the migration path from environment
|
|
222
|
-
* variable-based configuration to
|
|
216
|
+
* variable-based configuration to key manager storage.
|
|
223
217
|
*
|
|
224
218
|
* @param apiKey - The 32-character lowercase hexadecimal API key to migrate
|
|
225
219
|
* @param baseUrl - The Iterable API base URL for this key
|
|
226
220
|
* @param name - The name to assign (defaults to "default")
|
|
227
221
|
* @returns The unique ID of the migrated key (existing or newly created)
|
|
228
|
-
* @throws {Error} If validation fails or
|
|
222
|
+
* @throws {Error} If validation fails or storage fails
|
|
229
223
|
*/
|
|
230
224
|
migrateLegacyKey(apiKey: string, baseUrl: string, name?: string): Promise<string>;
|
|
231
225
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-manager.d.ts","sourceRoot":"","sources":["../src/key-manager.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"key-manager.d.ts","sourceRoot":"","sources":["../src/key-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/key-manager.js
CHANGED
|
@@ -1,66 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Secure API Key Management
|
|
2
|
+
* Secure API Key Management with cross-platform support
|
|
3
3
|
*
|
|
4
|
-
* This module provides secure storage and management of multiple Iterable API keys
|
|
5
|
-
* using macOS Keychain via the native `security` command-line tool.
|
|
4
|
+
* This module provides secure storage and management of multiple Iterable API keys.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
* - macOS:
|
|
9
|
-
* - Windows
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - API keys: Stored securely in macOS Keychain (never on disk)
|
|
13
|
-
* - Metadata: Stored in ~/.iterable-mcp/keys.json (names, IDs, endpoints, timestamps)
|
|
14
|
-
* - Lock file: ~/.iterable-mcp/keys.lock (prevents concurrent modifications)
|
|
6
|
+
* Storage Strategy:
|
|
7
|
+
* - macOS: API keys in Keychain, metadata in ~/.iterable-mcp/keys.json
|
|
8
|
+
* - Windows: API keys and metadata in ~/.iterable-mcp/keys.json
|
|
9
|
+
* - Linux: API keys and metadata in ~/.iterable-mcp/keys.json (mode 0o600)
|
|
10
|
+
* - Lock file: ~/.iterable-mcp/keys.lock prevents concurrent modifications
|
|
15
11
|
*
|
|
16
12
|
* Security Features:
|
|
17
|
-
* - Uses spawn() with argument arrays to prevent shell injection
|
|
18
|
-
* - File-based locking for concurrent access protection
|
|
19
|
-
* - Test-specific service name to isolate test keychain entries
|
|
20
13
|
* - API key format validation (32-char lowercase hex)
|
|
21
|
-
* - HTTPS-only URL validation
|
|
14
|
+
* - HTTPS-only URL validation (except localhost)
|
|
22
15
|
* - Duplicate key detection (both names and values)
|
|
16
|
+
* - File-based locking for concurrent access protection
|
|
17
|
+
* - Restrictive file permissions where supported (Linux/macOS)
|
|
23
18
|
*/
|
|
24
19
|
import { logger } from "@iterable/api";
|
|
25
|
-
import {
|
|
20
|
+
import { execFile } from "child_process";
|
|
26
21
|
import { randomUUID } from "crypto";
|
|
27
22
|
import { promises as fs } from "fs";
|
|
28
23
|
import os from "os";
|
|
29
24
|
import path from "path";
|
|
25
|
+
import { promisify } from "util";
|
|
30
26
|
import { isHttpsOrLocalhost, isLocalhostHost } from "./utils/url.js";
|
|
31
27
|
// Service name for keychain entries
|
|
32
28
|
// IMPORTANT: This must be constant to avoid data loss. Never use NODE_ENV here!
|
|
33
29
|
// Tests should use dependency injection with mock execSecurity instead.
|
|
34
30
|
const SERVICE_NAME = "iterable-mcp";
|
|
31
|
+
const execFileAsync = promisify(execFile);
|
|
35
32
|
/**
|
|
36
33
|
* Safely execute macOS security command with proper argument escaping
|
|
37
|
-
* Uses
|
|
34
|
+
* Uses execFile to prevent shell injection vulnerabilities
|
|
38
35
|
*/
|
|
39
36
|
async function execSecurityDefault(args) {
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stdout += data.toString();
|
|
48
|
-
});
|
|
49
|
-
child.stderr.on("data", (data) => {
|
|
50
|
-
stderr += data.toString();
|
|
51
|
-
});
|
|
52
|
-
child.on("error", (error) => {
|
|
53
|
-
reject(new Error(`Failed to execute security command: ${error.message}`));
|
|
54
|
-
});
|
|
55
|
-
child.on("close", (code) => {
|
|
56
|
-
if (code === 0) {
|
|
57
|
-
resolve(stdout.trim());
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
reject(new Error(`Security command failed with code ${code}: ${stderr.trim() || stdout.trim()}`));
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
37
|
+
try {
|
|
38
|
+
const { stdout } = await execFileAsync("security", args);
|
|
39
|
+
return stdout.trim();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error(`Security command failed: ${error.stderr?.toString().trim() || error.message}`);
|
|
43
|
+
}
|
|
64
44
|
}
|
|
65
45
|
export class KeyManager {
|
|
66
46
|
configDir;
|
|
@@ -69,25 +49,26 @@ export class KeyManager {
|
|
|
69
49
|
store = null;
|
|
70
50
|
saveLock = null;
|
|
71
51
|
execSecurity;
|
|
52
|
+
useKeychain;
|
|
72
53
|
/**
|
|
73
54
|
* Create a new KeyManager instance
|
|
74
55
|
*
|
|
75
56
|
* @param configDir - Optional custom config directory (defaults to ~/.iterable-mcp)
|
|
76
57
|
* @param execSecurity - Optional security command executor (for dependency injection in tests)
|
|
77
|
-
* @throws {Error} If not running on macOS and no mock executor is provided
|
|
78
58
|
*/
|
|
79
59
|
constructor(configDir, execSecurity) {
|
|
80
|
-
// Check if running on macOS (skip check in tests when mock is provided)
|
|
81
|
-
if (!execSecurity && process.platform !== "darwin") {
|
|
82
|
-
throw new Error("Key Manager only supports macOS. Windows and Linux users should use the ITERABLE_API_KEY environment variable.");
|
|
83
|
-
}
|
|
84
60
|
this.configDir = configDir || path.join(os.homedir(), ".iterable-mcp");
|
|
85
61
|
this.metadataFile = path.join(this.configDir, "keys.json");
|
|
86
62
|
this.lockFile = path.join(this.configDir, "keys.lock");
|
|
87
63
|
this.execSecurity = execSecurity || execSecurityDefault;
|
|
64
|
+
// Use Keychain on macOS (or when mock execSecurity provided for tests)
|
|
65
|
+
// Can be overridden via ITERABLE_MCP_FORCE_FILE_STORAGE=true
|
|
66
|
+
this.useKeychain =
|
|
67
|
+
(!!execSecurity || process.platform === "darwin") &&
|
|
68
|
+
process.env.ITERABLE_MCP_FORCE_FILE_STORAGE !== "true";
|
|
88
69
|
}
|
|
89
70
|
/**
|
|
90
|
-
* Validate metadata against keychain and clean up orphaned entries
|
|
71
|
+
* Validate metadata against keychain and clean up orphaned entries (macOS only)
|
|
91
72
|
*
|
|
92
73
|
* Checks if each key in metadata still exists in the keychain.
|
|
93
74
|
* Removes any metadata entries that no longer have corresponding keychain entries.
|
|
@@ -96,7 +77,8 @@ export class KeyManager {
|
|
|
96
77
|
* @returns Array of cleaned up key names (if any)
|
|
97
78
|
*/
|
|
98
79
|
async validateAndCleanup() {
|
|
99
|
-
|
|
80
|
+
// Only validate Keychain on macOS
|
|
81
|
+
if (!this.useKeychain || !this.store || this.store.keys.length === 0) {
|
|
100
82
|
return [];
|
|
101
83
|
}
|
|
102
84
|
const orphanedKeys = [];
|
|
@@ -396,15 +378,13 @@ export class KeyManager {
|
|
|
396
378
|
/**
|
|
397
379
|
* Add a new API key
|
|
398
380
|
*
|
|
399
|
-
*
|
|
400
|
-
* (name, base URL, timestamps) to disk. Each key is tightly coupled to
|
|
401
|
-
* its API endpoint.
|
|
381
|
+
* Stores an API key securely (Keychain on macOS, file on other platforms).
|
|
402
382
|
*
|
|
403
383
|
* @param name - User-friendly name for the key (must be unique)
|
|
404
384
|
* @param apiKey - 32-character lowercase hexadecimal Iterable API key
|
|
405
385
|
* @param baseUrl - Iterable API base URL (must be HTTPS)
|
|
406
386
|
* @returns The unique ID generated for this key
|
|
407
|
-
* @throws {Error} If the key name already exists, validation fails, or
|
|
387
|
+
* @throws {Error} If the key name already exists, validation fails, or storage fails
|
|
408
388
|
*/
|
|
409
389
|
async addKey(name, apiKey, baseUrl, envOverrides) {
|
|
410
390
|
if (!this.store) {
|
|
@@ -425,24 +405,6 @@ export class KeyManager {
|
|
|
425
405
|
}
|
|
426
406
|
// Generate unique ID
|
|
427
407
|
const id = this.generateId();
|
|
428
|
-
// Store in macOS Keychain using security command
|
|
429
|
-
// Using spawn with argument array to prevent shell injection
|
|
430
|
-
try {
|
|
431
|
-
await this.execSecurity([
|
|
432
|
-
"add-generic-password",
|
|
433
|
-
"-a",
|
|
434
|
-
id,
|
|
435
|
-
"-s",
|
|
436
|
-
SERVICE_NAME,
|
|
437
|
-
"-w",
|
|
438
|
-
apiKey,
|
|
439
|
-
"-U",
|
|
440
|
-
]);
|
|
441
|
-
}
|
|
442
|
-
catch (error) {
|
|
443
|
-
logger.error("Failed to store key in keychain", { error, id });
|
|
444
|
-
throw new Error(`Failed to store key in macOS Keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
445
|
-
}
|
|
446
408
|
// If this is the first key, make it active
|
|
447
409
|
const isActive = this.store.keys.length === 0;
|
|
448
410
|
// Create metadata
|
|
@@ -456,6 +418,30 @@ export class KeyManager {
|
|
|
456
418
|
? { env: envOverrides }
|
|
457
419
|
: {}),
|
|
458
420
|
};
|
|
421
|
+
// Store API key
|
|
422
|
+
if (this.useKeychain) {
|
|
423
|
+
// macOS: Store in Keychain
|
|
424
|
+
try {
|
|
425
|
+
await this.execSecurity([
|
|
426
|
+
"add-generic-password",
|
|
427
|
+
"-a",
|
|
428
|
+
id,
|
|
429
|
+
"-s",
|
|
430
|
+
SERVICE_NAME,
|
|
431
|
+
"-w",
|
|
432
|
+
apiKey,
|
|
433
|
+
"-U",
|
|
434
|
+
]);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
logger.error("Failed to store key in keychain", { error, id });
|
|
438
|
+
throw new Error(`Failed to store key in macOS Keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
// Windows/Linux: Store in JSON
|
|
443
|
+
metadata.apiKey = apiKey;
|
|
444
|
+
}
|
|
459
445
|
// Store metadata
|
|
460
446
|
this.store.keys.push(metadata);
|
|
461
447
|
await this.saveMetadata();
|
|
@@ -499,11 +485,11 @@ export class KeyManager {
|
|
|
499
485
|
/**
|
|
500
486
|
* Get a key by ID or name
|
|
501
487
|
*
|
|
502
|
-
* Retrieves the actual API key value from
|
|
488
|
+
* Retrieves the actual API key value from storage.
|
|
503
489
|
*
|
|
504
490
|
* @param idOrName - The unique ID or user-friendly name of the key
|
|
505
491
|
* @returns The API key value, or null if not found
|
|
506
|
-
* @throws {Error} If
|
|
492
|
+
* @throws {Error} If storage access fails
|
|
507
493
|
*/
|
|
508
494
|
async getKey(idOrName) {
|
|
509
495
|
if (!this.store) {
|
|
@@ -517,31 +503,40 @@ export class KeyManager {
|
|
|
517
503
|
if (!keyMeta) {
|
|
518
504
|
return null;
|
|
519
505
|
}
|
|
520
|
-
// Get
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
}
|
|
538
|
-
|
|
506
|
+
// Get API key
|
|
507
|
+
if (this.useKeychain) {
|
|
508
|
+
// macOS: Get from Keychain
|
|
509
|
+
try {
|
|
510
|
+
const apiKey = await this.execSecurity([
|
|
511
|
+
"find-generic-password",
|
|
512
|
+
"-a",
|
|
513
|
+
keyMeta.id,
|
|
514
|
+
"-s",
|
|
515
|
+
SERVICE_NAME,
|
|
516
|
+
"-w",
|
|
517
|
+
]);
|
|
518
|
+
if (!apiKey) {
|
|
519
|
+
logger.error("Key not found in keychain", { id: keyMeta.id });
|
|
520
|
+
throw new Error(`Key not found in macOS Keychain for ID ${keyMeta.id}`);
|
|
521
|
+
}
|
|
522
|
+
return apiKey;
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
logger.error("Failed to retrieve key from keychain", {
|
|
526
|
+
error,
|
|
527
|
+
id: keyMeta.id,
|
|
528
|
+
});
|
|
529
|
+
throw new Error(`Failed to retrieve key from macOS Keychain: ${error instanceof Error ? error.message : String(error)}`);
|
|
530
|
+
}
|
|
539
531
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
532
|
+
else {
|
|
533
|
+
// Windows/Linux: Get from JSON
|
|
534
|
+
if (!keyMeta.apiKey) {
|
|
535
|
+
logger.error("Key not found in metadata", { id: keyMeta.id });
|
|
536
|
+
throw new Error(`Key not found in storage for ID ${keyMeta.id}`);
|
|
537
|
+
}
|
|
538
|
+
return keyMeta.apiKey;
|
|
543
539
|
}
|
|
544
|
-
return apiKey;
|
|
545
540
|
}
|
|
546
541
|
/**
|
|
547
542
|
* Get the currently active key
|
|
@@ -550,7 +545,7 @@ export class KeyManager {
|
|
|
550
545
|
* Only one key can be active at a time.
|
|
551
546
|
*
|
|
552
547
|
* @returns The active API key value, or null if no key is active
|
|
553
|
-
* @throws {Error} If
|
|
548
|
+
* @throws {Error} If storage access fails
|
|
554
549
|
*/
|
|
555
550
|
async getActiveKey() {
|
|
556
551
|
if (!this.store) {
|
|
@@ -569,7 +564,7 @@ export class KeyManager {
|
|
|
569
564
|
* Get the active key metadata
|
|
570
565
|
*
|
|
571
566
|
* Returns metadata for the currently active key without retrieving the
|
|
572
|
-
* actual API key value
|
|
567
|
+
* actual API key value.
|
|
573
568
|
*
|
|
574
569
|
* @returns The active key metadata, or null if no key is active
|
|
575
570
|
* @throws {Error} If the key store is not initialized
|
|
@@ -616,10 +611,9 @@ export class KeyManager {
|
|
|
616
611
|
/**
|
|
617
612
|
* Delete a key by ID
|
|
618
613
|
*
|
|
619
|
-
* Removes a key from
|
|
614
|
+
* Removes a key from storage and the metadata store.
|
|
620
615
|
* The currently active key cannot be deleted - you must activate a different
|
|
621
|
-
* key first.
|
|
622
|
-
* and a warning will be displayed.
|
|
616
|
+
* key first.
|
|
623
617
|
*
|
|
624
618
|
* Note: Only accepts key ID (not name) since IDs are guaranteed unique.
|
|
625
619
|
*
|
|
@@ -643,38 +637,46 @@ export class KeyManager {
|
|
|
643
637
|
if (keyMeta.isActive) {
|
|
644
638
|
throw new Error(`Cannot delete the currently active key "${keyMeta.name}". Please activate a different key first.`);
|
|
645
639
|
}
|
|
646
|
-
// Delete from
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
error,
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
640
|
+
// Delete from storage
|
|
641
|
+
if (this.useKeychain) {
|
|
642
|
+
// macOS: Delete from Keychain
|
|
643
|
+
let keychainDeleted = false;
|
|
644
|
+
try {
|
|
645
|
+
await this.execSecurity([
|
|
646
|
+
"delete-generic-password",
|
|
647
|
+
"-a",
|
|
648
|
+
keyMeta.id,
|
|
649
|
+
"-s",
|
|
650
|
+
SERVICE_NAME,
|
|
651
|
+
]);
|
|
652
|
+
keychainDeleted = true;
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
logger.error("Failed to delete key from keychain", {
|
|
656
|
+
error,
|
|
657
|
+
id: keyMeta.id,
|
|
658
|
+
});
|
|
659
|
+
// Continue anyway to clean up metadata, but warn the user
|
|
660
|
+
}
|
|
661
|
+
// Remove from metadata
|
|
662
|
+
this.store.keys.splice(index, 1);
|
|
663
|
+
await this.saveMetadata();
|
|
664
|
+
if (!keychainDeleted) {
|
|
665
|
+
logger.warn("Key removed from metadata but may still exist in Keychain", {
|
|
666
|
+
id: keyMeta.id,
|
|
667
|
+
name: keyMeta.name,
|
|
668
|
+
});
|
|
669
|
+
console.warn(`⚠️ Warning: Key "${keyMeta.name}" removed from metadata but may still exist in Keychain.`);
|
|
670
|
+
console.warn(` To manually remove: security delete-generic-password -a "${keyMeta.id}" -s "${SERVICE_NAME}"`);
|
|
671
|
+
}
|
|
672
|
+
else {
|
|
673
|
+
logger.info("API key deleted", { id: keyMeta.id, name: keyMeta.name });
|
|
674
|
+
}
|
|
676
675
|
}
|
|
677
676
|
else {
|
|
677
|
+
// Windows/Linux: Just remove from JSON
|
|
678
|
+
this.store.keys.splice(index, 1);
|
|
679
|
+
await this.saveMetadata();
|
|
678
680
|
logger.info("API key deleted", { id: keyMeta.id, name: keyMeta.name });
|
|
679
681
|
}
|
|
680
682
|
}
|
|
@@ -698,12 +700,12 @@ export class KeyManager {
|
|
|
698
700
|
/**
|
|
699
701
|
* Find a key by its actual API key value
|
|
700
702
|
*
|
|
701
|
-
* Checks all stored keys
|
|
702
|
-
*
|
|
703
|
+
* Checks all stored keys to see if the given API key value already exists.
|
|
704
|
+
* Useful for preventing duplicate key values.
|
|
703
705
|
*
|
|
704
706
|
* @param apiKeyValue - The API key value to search for
|
|
705
707
|
* @returns The metadata of the matching key, or null if not found
|
|
706
|
-
* @throws {Error} If
|
|
708
|
+
* @throws {Error} If storage access fails
|
|
707
709
|
*/
|
|
708
710
|
async findKeyByValue(apiKeyValue) {
|
|
709
711
|
if (!this.store) {
|
|
@@ -715,20 +717,31 @@ export class KeyManager {
|
|
|
715
717
|
// Check each key to see if the value matches
|
|
716
718
|
for (const keyMeta of this.store.keys) {
|
|
717
719
|
try {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
720
|
+
let storedKey;
|
|
721
|
+
if (this.useKeychain) {
|
|
722
|
+
// macOS: Check Keychain
|
|
723
|
+
storedKey = await this.execSecurity([
|
|
724
|
+
"find-generic-password",
|
|
725
|
+
"-a",
|
|
726
|
+
keyMeta.id,
|
|
727
|
+
"-s",
|
|
728
|
+
SERVICE_NAME,
|
|
729
|
+
"-w",
|
|
730
|
+
]);
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
// Windows/Linux: Check JSON
|
|
734
|
+
if (!keyMeta.apiKey) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
storedKey = keyMeta.apiKey;
|
|
738
|
+
}
|
|
726
739
|
if (storedKey.trim() === apiKeyValue) {
|
|
727
740
|
return keyMeta;
|
|
728
741
|
}
|
|
729
742
|
}
|
|
730
743
|
catch {
|
|
731
|
-
// Skip keys that can't be retrieved (may have been manually deleted
|
|
744
|
+
// Skip keys that can't be retrieved (may have been manually deleted)
|
|
732
745
|
continue;
|
|
733
746
|
}
|
|
734
747
|
}
|
|
@@ -739,13 +752,13 @@ export class KeyManager {
|
|
|
739
752
|
*
|
|
740
753
|
* Adds an API key to the key manager with a default name if it doesn't
|
|
741
754
|
* already exist. This is used during the migration path from environment
|
|
742
|
-
* variable-based configuration to
|
|
755
|
+
* variable-based configuration to key manager storage.
|
|
743
756
|
*
|
|
744
757
|
* @param apiKey - The 32-character lowercase hexadecimal API key to migrate
|
|
745
758
|
* @param baseUrl - The Iterable API base URL for this key
|
|
746
759
|
* @param name - The name to assign (defaults to "default")
|
|
747
760
|
* @returns The unique ID of the migrated key (existing or newly created)
|
|
748
|
-
* @throws {Error} If validation fails or
|
|
761
|
+
* @throws {Error} If validation fails or storage fails
|
|
749
762
|
*/
|
|
750
763
|
async migrateLegacyKey(apiKey, baseUrl, name = "default") {
|
|
751
764
|
// Check if a key with this name already exists
|