From cfc20046877aec42de67dacb881b07b37a80212b Mon Sep 17 00:00:00 2001 From: Greg Methvin Date: Tue, 18 Nov 2025 22:47:32 -0800 Subject: [PATCH] Update keychain references --- src/install.ts | 7 +-- src/keys-cli.ts | 11 ++-- src/utils/formatting.ts | 16 +++++- src/utils/ui.ts | 2 +- tests/integration/mcp-protocol.test.ts | 2 +- tests/unit/ui-formatting.test.ts | 76 +++++++++++++++++++++++++- 6 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/install.ts b/src/install.ts index c8df236..0035f85 100644 --- a/src/install.ts +++ b/src/install.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "url"; import { promisify } from "util"; import { getKeyManager } from "./key-manager.js"; +import { getKeyStorageMessage } from "./utils/formatting.js"; const { dirname, join } = path; @@ -341,11 +342,7 @@ export const setupMcpServer = async (): Promise => { "Security Features", [ "• API keys prompted interactively (never in shell history)", - process.platform === "darwin" - ? "• Keys are stored securely in the macOS Keychain" - : process.platform === "win32" - ? "• Keys are stored in ~/.iterable-mcp/keys.json" - : "• Keys are stored in ~/.iterable-mcp/keys.json with restricted permissions", + getKeyStorageMessage(true), "• Each key coupled to its endpoint (US/EU/custom)", ], { icon: icons.lock, theme: "info", padding: 1 } diff --git a/src/keys-cli.ts b/src/keys-cli.ts index 9a84ef3..3ae8c1f 100644 --- a/src/keys-cli.ts +++ b/src/keys-cli.ts @@ -14,6 +14,7 @@ import { promisify } from "util"; const { dirname, join } = path; import { getSpinner, loadUi } from "./utils/cli-env.js"; +import { getKeyStorageMessage } from "./utils/formatting.js"; import { promptForApiKey } from "./utils/password-prompt.js"; const execFileAsync = promisify(execFile); @@ -149,7 +150,7 @@ export async function handleKeysCommand(): Promise { chalk.cyan("keys activate ") + " to switch between keys", "Use " + chalk.cyan("keys add") + " to add a new API key", - "Keys are stored securely in macOS Keychain", + getKeyStorageMessage(), ]; showBox("Quick Tips", tips, { @@ -316,7 +317,7 @@ export async function handleKeysCommand(): Promise { ); console.log(); - showSuccess("Your API key is now stored securely in macOS Keychain"); + showSuccess(`Your API key "${name}" is now stored`); // Offer to set newly added key as active const { activateNow } = await inquirer.prompt([ @@ -656,7 +657,7 @@ export async function handleKeysCommand(): Promise { console.log(formatKeyValue("ID", resolved, chalk.gray)); console.log(); - showSuccess("Key removed from macOS Keychain"); + showSuccess("Key removed successfully"); } catch (error) { spinner.fail("Failed to delete key"); showError(error instanceof Error ? error.message : "Unknown error"); @@ -716,9 +717,7 @@ export async function handleKeysCommand(): Promise { const tips = [ "API keys are prompted interactively - never stored in shell history", "Each API key is tightly coupled to its endpoint (US/EU/custom)", - process.platform === "darwin" - ? "Keys are stored securely in macOS Keychain" - : "Keys are stored in ~/.iterable-mcp/keys.json with restricted permissions", + getKeyStorageMessage(), "Use 'keys list' to see all your keys and their details", "The active key (● ACTIVE) is what your AI tools will use", "To update a key: delete the old one and add a new one", diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts index a42757b..4981219 100644 --- a/src/utils/formatting.ts +++ b/src/utils/formatting.ts @@ -1,5 +1,5 @@ /** - * Test-friendly, chalk-free formatter for Keychain choice labels. + * Test-friendly, chalk-free formatter for stored key choice labels. * Production coloring/wrapping is applied in ui.ts. */ export function formatKeychainChoiceLabelPlain( @@ -18,3 +18,17 @@ export function formatKeychainChoiceLabelPlain( : ""; return `${activeBadge}${name} ${endpoint}${flags}`; } + +/** + * Get platform-specific storage description for tips/help text + * @param bulletPoint - Whether to include a bullet point prefix (default: false) + */ +export function getKeyStorageMessage(bulletPoint = false): string { + const prefix = bulletPoint ? "• " : ""; + const message = + process.platform === "darwin" + ? "Keys are stored securely in macOS Keychain" + : "Keys are stored in ~/.iterable-mcp/keys.json" + + (process.platform === "win32" ? "" : " with restricted permissions"); + return prefix + message; +} diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 785f90a..62f95c9 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -441,7 +441,7 @@ export function showProgress(message: string, done = false): void { } /** - * Format a macOS Keychain entry label for selection lists + * Format a stored key entry label for selection lists */ export function formatKeychainChoiceLabel( name: string, diff --git a/tests/integration/mcp-protocol.test.ts b/tests/integration/mcp-protocol.test.ts index d21c2d6..2ad2411 100644 --- a/tests/integration/mcp-protocol.test.ts +++ b/tests/integration/mcp-protocol.test.ts @@ -70,7 +70,7 @@ describe("MCP Protocol Integration Tests", () => { if (!isValidApiKey(resolvedApiKey)) { throw new Error( - "No valid API key found. Set ITERABLE_API_KEY or add/activate a key in macOS Keychain." + "No valid API key found. Set ITERABLE_API_KEY or add/activate a key using 'iterable-mcp keys'." ); } diff --git a/tests/unit/ui-formatting.test.ts b/tests/unit/ui-formatting.test.ts index b8a84f9..a74c331 100644 --- a/tests/unit/ui-formatting.test.ts +++ b/tests/unit/ui-formatting.test.ts @@ -1,7 +1,10 @@ /* eslint-disable simple-import-sort/imports */ -import { describe, it, expect } from "@jest/globals"; +import { describe, it, expect, beforeEach, afterEach } from "@jest/globals"; -import { formatKeychainChoiceLabelPlain } from "../../src/utils/formatting"; +import { + formatKeychainChoiceLabelPlain, + getKeyStorageMessage, +} from "../../src/utils/formatting"; describe("formatKeychainChoiceLabel", () => { it("includes name and endpoint, excludes id", () => { @@ -49,3 +52,72 @@ describe("formatKeychainChoiceLabel", () => { expect(label).toContain("Sends: Off"); }); }); + +describe("getKeyStorageMessage", () => { + let originalPlatform: string; + + beforeEach(() => { + originalPlatform = process.platform; + }); + + afterEach(() => { + Object.defineProperty(process, "platform", { + value: originalPlatform, + writable: true, + configurable: true, + }); + }); + + it("returns macOS Keychain message on darwin", () => { + Object.defineProperty(process, "platform", { + value: "darwin", + writable: true, + configurable: true, + }); + expect(getKeyStorageMessage()).toBe( + "Keys are stored securely in macOS Keychain" + ); + }); + + it("returns file message on win32", () => { + Object.defineProperty(process, "platform", { + value: "win32", + writable: true, + configurable: true, + }); + expect(getKeyStorageMessage()).toBe( + "Keys are stored in ~/.iterable-mcp/keys.json" + ); + }); + + it("returns file with permissions message on linux", () => { + Object.defineProperty(process, "platform", { + value: "linux", + writable: true, + configurable: true, + }); + expect(getKeyStorageMessage()).toBe( + "Keys are stored in ~/.iterable-mcp/keys.json with restricted permissions" + ); + }); + + it("adds bullet point prefix when requested", () => { + Object.defineProperty(process, "platform", { + value: "darwin", + writable: true, + configurable: true, + }); + expect(getKeyStorageMessage(true)).toBe( + "• Keys are stored securely in macOS Keychain" + ); + }); + + it("omits bullet point by default", () => { + Object.defineProperty(process, "platform", { + value: "darwin", + writable: true, + configurable: true, + }); + expect(getKeyStorageMessage()).not.toContain("• "); + }); +});