Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -341,11 +342,7 @@ export const setupMcpServer = async (): Promise<void> => {
"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 }
Expand Down
11 changes: 5 additions & 6 deletions src/keys-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -149,7 +150,7 @@ export async function handleKeysCommand(): Promise<void> {
chalk.cyan("keys activate <name>") +
" 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, {
Expand Down Expand Up @@ -316,7 +317,7 @@ export async function handleKeysCommand(): Promise<void> {
);
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([
Expand Down Expand Up @@ -656,7 +657,7 @@ export async function handleKeysCommand(): Promise<void> {
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");
Expand Down Expand Up @@ -716,9 +717,7 @@ export async function handleKeysCommand(): Promise<void> {
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",
Expand Down
16 changes: 15 additions & 1 deletion src/utils/formatting.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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;
}
2 changes: 1 addition & 1 deletion src/utils/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/mcp-protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'."
);
}

Expand Down
76 changes: 74 additions & 2 deletions tests/unit/ui-formatting.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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("• ");
});
});