Skip to content

Commit 35e78a9

Browse files
committed
Improve key management UX
1 parent 0058f76 commit 35e78a9

7 files changed

Lines changed: 270 additions & 228 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ See the [full command reference](COMMANDS.md) for all 109 commands with paramete
119119
| `--columns <cols>` | Comma-separated columns for table output |
120120
| `--json <data>` | Pass raw JSON (use `-` for stdin) |
121121
| `--file <path>` | Read JSON input from a file |
122+
| `--key, -k <name>` | Use a specific stored key (overrides env var and active key) |
122123
| `--force, -f` | Skip confirmation prompts for destructive commands |
123124

124125
## Output Formats
@@ -147,8 +148,8 @@ Without `--columns`, all fields are shown.
147148

148149
| Variable | Description | Default |
149150
|----------|-------------|---------|
150-
| `ITERABLE_API_KEY` | API key (overrides key manager) ||
151-
| `ITERABLE_BASE_URL` | API base URL (overrides key manager) | `https://api.iterable.com` |
151+
| `ITERABLE_API_KEY` | API key (overrides key manager; `--key` flag takes precedence) ||
152+
| `ITERABLE_BASE_URL` | API base URL (used with env var key only) | `https://api.iterable.com` |
152153
| `ITERABLE_DEBUG` | Enable debug logging (HTTP requests/responses to stderr) | `false` |
153154
| `ITERABLE_DEBUG_VERBOSE` | Include response bodies in debug output (may contain PII) | `false` |
154155

src/config.ts

Lines changed: 47 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,67 +15,78 @@ export interface CliConfig {
1515
* Load configuration.
1616
*
1717
* Resolution order:
18-
* 1. ITERABLE_API_KEY env var (if set, use directly -- enables CI/scripting override)
19-
* 2. Key manager active key
20-
* 3. Error with guidance
18+
* 1. --key flag (specific stored key by name or ID)
19+
* 2. ITERABLE_API_KEY env var (enables CI/scripting override)
20+
* 3. Key manager active key
2121
*/
22-
export async function loadCliConfig(): Promise<CliConfig> {
23-
const baseUrlFromEnv =
24-
process.env.ITERABLE_BASE_URL ?? "https://api.iterable.com";
22+
const DEFAULT_BASE_URL = "https://api.iterable.com";
23+
24+
export async function loadCliConfig(keyNameOrId?: string): Promise<CliConfig> {
25+
if (keyNameOrId && !isTestEnv()) {
26+
const keyManager = getKeyManager();
27+
const keys = await keyManager.listKeys();
28+
const keyMeta = keys.find(
29+
(k) => k.name === keyNameOrId || k.id === keyNameOrId
30+
);
31+
if (!keyMeta) {
32+
throw new CliError(
33+
`Key not found: "${keyNameOrId}". Run "${COMMAND_NAME} keys list" to see available keys.`,
34+
2
35+
);
36+
}
37+
const apiKey = await keyManager.getKey(keyMeta.id);
38+
if (!apiKey) {
39+
throw new CliError(
40+
`Failed to retrieve key "${keyNameOrId}". Try "${COMMAND_NAME} keys update ${keyNameOrId}".`,
41+
1
42+
);
43+
}
44+
return { apiKey, baseUrl: keyMeta.baseUrl };
45+
}
2546

2647
if (process.env.ITERABLE_API_KEY) {
27-
return { apiKey: process.env.ITERABLE_API_KEY, baseUrl: baseUrlFromEnv };
48+
return {
49+
apiKey: process.env.ITERABLE_API_KEY,
50+
baseUrl: process.env.ITERABLE_BASE_URL ?? DEFAULT_BASE_URL,
51+
};
2852
}
2953

3054
if (!isTestEnv()) {
3155
try {
3256
const keyManager = getKeyManager();
33-
await keyManager.initialize();
3457

3558
if (await keyManager.hasKeys()) {
3659
if (!(await keyManager.hasActiveKey())) {
3760
throw new CliError(
38-
`No active API key. Run "${COMMAND_NAME} keys activate <name>" to activate one, or set ITERABLE_API_KEY environment variable.`,
61+
`No active API key. Run "${COMMAND_NAME} keys activate <name>", use --key <name>, or set ITERABLE_API_KEY.`,
3962
2
4063
);
4164
}
4265
const apiKey = await keyManager.getActiveKey();
43-
if (apiKey) {
44-
let baseUrl = baseUrlFromEnv;
45-
const meta = await keyManager.getActiveKeyMetadata();
46-
if (meta?.baseUrl) {
47-
baseUrl = meta.baseUrl;
48-
}
49-
return { apiKey, baseUrl };
66+
const meta = await keyManager.getActiveKeyMetadata();
67+
if (apiKey && meta) {
68+
return { apiKey, baseUrl: meta.baseUrl };
5069
}
5170
}
5271
} catch (error: unknown) {
5372
if (error instanceof CliError) throw error;
5473

55-
const errorMessage =
56-
error instanceof Error ? error.message : String(error);
57-
58-
if (
59-
errorMessage.includes("Key store not initialized") ||
60-
errorMessage.includes("No API key found")
61-
) {
62-
logger.debug("No keys in key manager, falling back to error");
63-
} else {
64-
const sanitizedMessage = sanitizeString(errorMessage);
65-
logger.error("Unexpected error loading from key manager", {
66-
error: sanitizedMessage,
67-
});
68-
console.error(
69-
// eslint-disable-line no-console
70-
"Warning: Failed to load API key from key storage:",
71-
sanitizedMessage
72-
);
73-
}
74+
const sanitizedMessage = sanitizeString(
75+
error instanceof Error ? error.message : String(error)
76+
);
77+
logger.error("Failed to load from key manager", {
78+
error: sanitizedMessage,
79+
});
80+
console.error(
81+
// eslint-disable-line no-console
82+
"Warning: Failed to load API key from key storage:",
83+
sanitizedMessage
84+
);
7485
}
7586
}
7687

7788
throw new CliError(
78-
`No API key found. Run "${COMMAND_NAME} keys add" to add one, or set ITERABLE_API_KEY environment variable.`,
89+
`No API key found. Run "${COMMAND_NAME} keys add" to add one, or set ITERABLE_API_KEY.`,
7990
2
8091
);
8192
}

src/index.ts

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
import { IterableClient } from "@iterable/api";
4+
import chalk from "chalk";
45
import { readFileSync } from "fs";
56
import { z } from "zod";
67

@@ -42,7 +43,8 @@ async function main(): Promise<void> {
4243
if (parsed.category === "keys") {
4344
const { handleKeysCommand } = await import("./keys-cli.js");
4445
await handleKeysCommand(
45-
parsed.action ? [parsed.action, ...parsed.rest] : []
46+
parsed.action ? [parsed.action, ...parsed.rest] : [],
47+
parsed.globalFlags.key
4648
);
4749
return;
4850
}
@@ -74,7 +76,7 @@ async function main(): Promise<void> {
7476
return;
7577
}
7678

77-
const config = await loadCliConfig();
79+
const config = await loadCliConfig(parsed.globalFlags.key);
7880
const debug =
7981
process.env.ITERABLE_DEBUG === "true" ||
8082
process.env.ITERABLE_DEBUG_VERBOSE === "true";
@@ -151,52 +153,41 @@ main().catch(async (error: unknown) => {
151153
IterableResponseValidationError,
152154
} = await import("@iterable/api");
153155

154-
/* eslint-disable no-console */
156+
const err = (msg: string) => console.error(chalk.red(`✖ ${msg}`)); // eslint-disable-line no-console
157+
const hint = (msg: string) => console.error(chalk.dim(` ${msg}`)); // eslint-disable-line no-console
158+
155159
if (error instanceof CliError) {
156-
console.error(error.message);
160+
err(error.message);
157161
process.exit(error.exitCode);
158162
}
159163

160164
if (error instanceof z.ZodError) {
161-
const details = error.issues
162-
.map((i) => `${i.path.join(".")}: ${i.message}`)
163-
.join(", ");
164-
console.error(`Validation error: ${details}`);
165+
err("Validation error");
166+
for (const issue of error.issues) {
167+
hint(`${issue.path.join(".")}: ${issue.message}`);
168+
}
165169
process.exit(2);
166170
}
167171

168-
if (error instanceof IterableApiError) {
169-
console.error(`API error (${error.statusCode}): ${error.message}`);
170-
if (error.endpoint) console.error(` Endpoint: ${error.endpoint}`);
171-
if (error.statusCode === 401) {
172-
console.error(
173-
` Run '${COMMAND_NAME} keys add' to configure your API key`
174-
);
172+
if (
173+
error instanceof IterableApiError ||
174+
error instanceof IterableRawError ||
175+
error instanceof IterableResponseValidationError
176+
) {
177+
err(`${error.message} (${error.statusCode})`);
178+
if (error.endpoint) hint(`Endpoint: ${error.endpoint}`);
179+
if (error instanceof IterableApiError && error.statusCode === 401) {
180+
hint(`Run '${COMMAND_NAME} keys add' to configure your API key`);
175181
}
176182
process.exit(1);
177183
}
178184

179-
if (error instanceof IterableRawError) {
180-
console.error(`API error (${error.statusCode}): ${error.message}`);
181-
if (error.endpoint) console.error(` Endpoint: ${error.endpoint}`);
182-
process.exit(1);
183-
}
184-
185-
if (error instanceof IterableResponseValidationError) {
186-
console.error(
187-
`Response validation error (${error.statusCode}): ${error.message}`
188-
);
189-
if (error.endpoint) console.error(` Endpoint: ${error.endpoint}`);
190-
process.exit(1);
191-
}
192-
193185
if (error instanceof IterableNetworkError) {
194-
console.error(`Network error: ${error.message}`);
186+
err(error.message);
195187
process.exit(1);
196188
}
197-
/* eslint-enable no-console */
198189

199190
const msg = error instanceof Error ? error.message : String(error);
200-
console.error(`Error: ${msg}`); // eslint-disable-line no-console
191+
err(msg);
201192
process.exit(1);
202193
});

0 commit comments

Comments
 (0)