diff --git a/packages/server/src/server/persisted-config.test.ts b/packages/server/src/server/persisted-config.test.ts index cb98d050a4..63f18e2b5e 100644 --- a/packages/server/src/server/persisted-config.test.ts +++ b/packages/server/src/server/persisted-config.test.ts @@ -765,6 +765,34 @@ describe("config.json saved with a UTF-8 byte order mark", () => { }); }); +describe("readPersistedConfig with an unreadable config.json", () => { + test("names the file when it is not valid JSON", () => { + const home = createTempHome(); + const configPath = path.join(home, "config.json"); + try { + writeFileSync(configPath, '{"version":1,'); + + expect(() => readPersistedConfig(home)).toThrow(`[Config] Invalid JSON in ${configPath}: `); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); + + test("names the file and the field when it does not match the schema", () => { + const home = createTempHome(); + const configPath = path.join(home, "config.json"); + try { + writeFileSync(configPath, '{"daemon":{"listen":5}}'); + + expect(() => readPersistedConfig(home)).toThrow( + `[Config] Invalid config in ${configPath}:\n - daemon.listen: `, + ); + } finally { + rmSync(home, { recursive: true, force: true }); + } + }); +}); + describe.skipIf(process.platform === "win32")("persisted config file permissions", () => { test("initializes config.json with private permissions", () => { const home = createTempHome(); diff --git a/packages/server/src/server/persisted-config.ts b/packages/server/src/server/persisted-config.ts index ad78ee1b2c..40c4150907 100644 --- a/packages/server/src/server/persisted-config.ts +++ b/packages/server/src/server/persisted-config.ts @@ -436,6 +436,29 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per }); } + const config = parseConfigFile(configPath, raw); + log?.info(`Loaded from ${configPath}`); + return config; +} + +/** Observe the file without initializing a home, identity, or default configuration. */ +export function readPersistedConfig( + paseoHome: string, + options: { defaultsIfMissing?: boolean } = {}, +): PersistedConfig { + const configPath = getConfigPath(paseoHome); + let raw: string; + try { + raw = readFileSync(configPath, "utf8"); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") + return options.defaultsIfMissing ? structuredClone(DEFAULT_PERSISTED_CONFIG) : {}; + throw error; + } + return parseConfigFile(configPath, raw); +} + +function parseConfigFile(configPath: string, raw: string): PersistedConfig { let parsed: unknown; try { parsed = parseConfigText(raw); @@ -446,37 +469,16 @@ export function loadPersistedConfig(paseoHome: string, logger?: LoggerLike): Per }); } - const migrated = stripRemovedConfigFields(parsed); - const result = PersistedConfigSchema.safeParse(migrated); + const result = PersistedConfigSchema.safeParse(stripRemovedConfigFields(parsed)); if (!result.success) { const issues = result.error.issues .map((i) => ` - ${i.path.join(".")}: ${i.message}`) .join("\n"); throw new Error(`[Config] Invalid config in ${configPath}:\n${issues}`); } - - log?.info(`Loaded from ${configPath}`); return result.data as PersistedConfig; } -/** Observe the file without initializing a home, identity, or default configuration. */ -export function readPersistedConfig( - paseoHome: string, - options: { defaultsIfMissing?: boolean } = {}, -): PersistedConfig { - let raw: string; - try { - raw = readFileSync(getConfigPath(paseoHome), "utf8"); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === "ENOENT") - return options.defaultsIfMissing ? structuredClone(DEFAULT_PERSISTED_CONFIG) : {}; - throw error; - } - return PersistedConfigSchema.parse( - stripRemovedConfigFields(parseConfigText(raw)), - ) as PersistedConfig; -} - /** Editors such as Windows Notepad save UTF-8 with a byte order mark, which JSON.parse rejects. */ function parseConfigText(raw: string): unknown { return JSON.parse(raw.replace(/^\uFEFF/, ""));