@kill-switch/cli 0.3.0 → 0.3.1

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.
@@ -1,5 +1,16 @@
1
1
  import { loadConfig, saveConfig, CONFIG_FILE, DEFAULT_API_URL } from "../config.js";
2
2
  import { outputJson } from "../output.js";
3
+ /** Mask a secret so it never lands in logs / CI output in full. */
4
+ function maskKey(v) {
5
+ const s = String(v ?? "");
6
+ return s.length > 12 ? `${s.slice(0, 12)}…${s.slice(-2)}` : "****";
7
+ }
8
+ /** Return a copy of the config with secret values masked unless `reveal`. */
9
+ function redactConfig(cfg, reveal) {
10
+ if (reveal || !cfg.apiKey)
11
+ return cfg;
12
+ return { ...cfg, apiKey: maskKey(cfg.apiKey) };
13
+ }
3
14
  export function registerConfigCommands(program) {
4
15
  const config = program.command("config").description("Manage CLI configuration");
5
16
  config
@@ -19,10 +30,13 @@ export function registerConfigCommands(program) {
19
30
  config
20
31
  .command("get <key>")
21
32
  .description("Get a config value")
22
- .action((key) => {
33
+ .option("--reveal", "Show secret values (e.g. apiKey) in full")
34
+ .action((key, opts) => {
23
35
  const json = program.opts().json;
24
36
  const cfg = loadConfig();
25
- const value = cfg[key];
37
+ let value = cfg[key];
38
+ if (key === "apiKey" && value && !opts.reveal)
39
+ value = maskKey(value);
26
40
  if (json) {
27
41
  outputJson({ [key]: value ?? null });
28
42
  }
@@ -49,11 +63,12 @@ export function registerConfigCommands(program) {
49
63
  .command("list")
50
64
  .alias("ls")
51
65
  .description("Show all config values")
52
- .action(() => {
66
+ .option("--reveal", "Show secret values (e.g. apiKey) in full")
67
+ .action((opts) => {
53
68
  const json = program.opts().json;
54
69
  const cfg = loadConfig();
55
70
  if (json) {
56
- outputJson(cfg);
71
+ outputJson(redactConfig(cfg, !!opts?.reveal));
57
72
  }
58
73
  else {
59
74
  const entries = Object.entries(cfg);
@@ -62,7 +77,7 @@ export function registerConfigCommands(program) {
62
77
  }
63
78
  else {
64
79
  for (const [k, v] of entries) {
65
- const display = k === "apiKey" ? String(v).substring(0, 16) + "..." : String(v);
80
+ const display = k === "apiKey" && !opts?.reveal ? maskKey(v) : String(v);
66
81
  console.log(`${k.padEnd(12)} ${display}`);
67
82
  }
68
83
  }
package/dist/config.d.ts CHANGED
@@ -19,7 +19,10 @@ export declare function deleteConfig(): void;
19
19
  */
20
20
  export declare function resolveApiKey(flagKey?: string): string | undefined;
21
21
  /**
22
- * Resolve the API URL.
22
+ * Resolve the API URL. The resolved endpoint receives the user's API key, so we
23
+ * refuse anything that isn't https:// (http:// allowed only for localhost) —
24
+ * this prevents a poisoned env var / config from redirecting the key over an
25
+ * insecure or unexpected channel (security: M-1).
23
26
  */
24
27
  export declare function resolveApiUrl(flagUrl?: string): string;
25
28
  export { CONFIG_DIR, CONFIG_FILE, DEFAULT_API_URL };
package/dist/config.js CHANGED
@@ -40,10 +40,27 @@ export function deleteConfig() {
40
40
  export function resolveApiKey(flagKey) {
41
41
  return process.env.KILL_SWITCH_API_KEY || flagKey || loadConfig().apiKey;
42
42
  }
43
+ const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
43
44
  /**
44
- * Resolve the API URL.
45
+ * Resolve the API URL. The resolved endpoint receives the user's API key, so we
46
+ * refuse anything that isn't https:// (http:// allowed only for localhost) —
47
+ * this prevents a poisoned env var / config from redirecting the key over an
48
+ * insecure or unexpected channel (security: M-1).
45
49
  */
46
50
  export function resolveApiUrl(flagUrl) {
47
- return process.env.KILL_SWITCH_API_URL || flagUrl || loadConfig().apiUrl || DEFAULT_API_URL;
51
+ const url = process.env.KILL_SWITCH_API_URL || flagUrl || loadConfig().apiUrl || DEFAULT_API_URL;
52
+ let parsed;
53
+ try {
54
+ parsed = new URL(url);
55
+ }
56
+ catch {
57
+ throw new Error(`Invalid API URL: "${url}". Set a valid https:// URL.`);
58
+ }
59
+ const localhost = LOCAL_HOSTS.has(parsed.hostname);
60
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && localhost)) {
61
+ throw new Error(`Refusing to use API URL "${url}": it must be https:// (http:// allowed only for localhost). ` +
62
+ `This protects your API key from being sent over an insecure channel.`);
63
+ }
64
+ return url;
48
65
  }
49
66
  export { CONFIG_DIR, CONFIG_FILE, DEFAULT_API_URL };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kill-switch/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Kill Switch CLI — monitor cloud spending, kill runaway services from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "test": "vitest run"
14
14
  },
15
15
  "dependencies": {
16
- "@kill-switch/agent-guard": "^0.1.0",
16
+ "@kill-switch/agent-guard": "^0.1.1",
17
17
  "@kill-switch/sdk": "^0.1.0",
18
18
  "chalk": "^5.6.2",
19
19
  "commander": "^12.1.0",