From e1db8e4fe7164716dad716a4402690289511d087 Mon Sep 17 00:00:00 2001 From: mooncitydev Date: Mon, 27 Apr 2026 04:27:48 +0900 Subject: [PATCH] fix config: clear errors on bad settings and mask api key in list hey jup team, mooncitydev here, did a pass on the config path and found two things worth fixing first is settings.json: if the file is corrupt or only half edited, JSON.parse threw a raw SyntaxError and every subcommand failed with a stack dump and no file path, so it was not obvious to delete the file and reset. load() now wraps parse in try/catch and throws a short message with the real path and option to remove the file for defaults second is jup config list: it printed the full portal api key in both table and json, which is easy to leak in screen shares, logs, or when pasting output. the key is still in ~/.config/jup/settings.json for tools that need it, but list now shows a token is set using the literal instead of the real string adds config.test for the parse failure and a couple edge cases. cheers Made-with: Cursor --- src/commands/ConfigCommand.ts | 16 +++++++-- src/lib/Config.test.ts | 66 +++++++++++++++++++++++++++++++++++ src/lib/Config.ts | 25 +++++++++---- 3 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 src/lib/Config.test.ts diff --git a/src/commands/ConfigCommand.ts b/src/commands/ConfigCommand.ts index 4034cae..bde73c8 100644 --- a/src/commands/ConfigCommand.ts +++ b/src/commands/ConfigCommand.ts @@ -27,13 +27,14 @@ export class ConfigCommand { private static list(): void { const settings = Config.load(); if (Output.isJson()) { - Output.json(settings); + Output.json(this.redactForDisplay(settings)); return; } - const data = Object.entries(settings).map(([key, value]) => ({ + const display = this.redactForDisplay(settings); + const data = Object.entries(display).map(([key, value]) => ({ setting: key, - value: value ? String(value) : "", + value: value != null && value !== "" ? String(value) : "", })); Output.table({ type: "horizontal", @@ -42,6 +43,15 @@ export class ConfigCommand { }); } + private static redactForDisplay( + settings: ReturnType + ): ReturnType { + if (!settings.apiKey) { + return settings; + } + return { ...settings, apiKey: "" }; + } + private static set(opts: { activeKey?: string; output?: "table" | "json"; diff --git a/src/lib/Config.test.ts b/src/lib/Config.test.ts new file mode 100644 index 0000000..6564b48 --- /dev/null +++ b/src/lib/Config.test.ts @@ -0,0 +1,66 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +import { Config } from "./Config.ts"; + +const TEST_DIR = join(tmpdir(), `jup-config-test-${Date.now()}`); +const origSettingsFile = Config.SETTINGS_FILE; + +beforeEach(() => { + mkdirSync(TEST_DIR, { recursive: true }); + // @ts-expect-error test override + Config.SETTINGS_FILE = join(TEST_DIR, "settings.json"); +}); + +afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); + // @ts-expect-error restore + Config.SETTINGS_FILE = origSettingsFile; +}); + +describe("Config.load", () => { + test("uses defaults when file is missing", () => { + const s = Config.load(); + expect(s).toEqual({ + activeKey: "default", + output: "table", + }); + expect(s.apiKey).toBeUndefined(); + }); + + test("throws a clear error when settings.json is not valid json", () => { + writeFileSync( + join(TEST_DIR, "settings.json"), + "{ broken json no closing brace", + "utf-8" + ); + expect(() => Config.load()).toThrow( + /Could not parse .*settings\.json.*Fix the file or remove it/ + ); + }); + + test("loads valid settings", () => { + writeFileSync( + join(TEST_DIR, "settings.json"), + JSON.stringify( + { activeKey: "main", output: "json", apiKey: "secret" }, + null, + 2 + ) + ); + expect(Config.load()).toEqual({ + activeKey: "main", + output: "json", + apiKey: "secret", + }); + }); + + test("treats non-object root as empty object for fields", () => { + writeFileSync(join(TEST_DIR, "settings.json"), "[]"); + const s = Config.load(); + expect(s.activeKey).toBe("default"); + expect(s.output).toBe("table"); + }); +}); diff --git a/src/lib/Config.ts b/src/lib/Config.ts index eefc0df..8f0708b 100644 --- a/src/lib/Config.ts +++ b/src/lib/Config.ts @@ -32,17 +32,30 @@ export class Config { if (!existsSync(this.SETTINGS_FILE)) { return { ...DEFAULT_SETTINGS }; } - const raw = JSON.parse(readFileSync(this.SETTINGS_FILE, "utf-8")); + const contents = readFileSync(this.SETTINGS_FILE, "utf-8"); + let raw: unknown; + try { + raw = JSON.parse(contents) as unknown; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + throw new Error( + `Could not parse ${this.SETTINGS_FILE}: ${message}. Fix the file or remove it to use defaults.` + ); + } + const o = + typeof raw === "object" && raw !== null + ? (raw as Record) + : null; return { activeKey: - typeof raw.activeKey === "string" - ? raw.activeKey + typeof o?.activeKey === "string" + ? o.activeKey : DEFAULT_SETTINGS.activeKey, output: - raw.output === "table" || raw.output === "json" - ? raw.output + o?.output === "table" || o?.output === "json" + ? o.output : DEFAULT_SETTINGS.output, - apiKey: typeof raw.apiKey === "string" ? raw.apiKey : undefined, + apiKey: typeof o?.apiKey === "string" ? o.apiKey : undefined, }; }