import fs from "fs/promises" import { Lock } from "./lock" /** * Shared persistence for small JSON-object credential stores (auth.json, * mcp-auth.json). Every write goes through a temp file in the same directory * followed by a rename, so a crash or concurrent reader never observes a torn * file, and every read-modify-write cycle is serialized behind the in-process * Lock keyed by file path. * * A file that exists but cannot be parsed is fatal on the write path: * proceeding with `{}` would rewrite the store containing only the entry * being saved and silently destroy every other credential. The corrupt file * is backed up alongside and the write throws. The read path degrades to * `{}` so the CLI still boots. */ export namespace JsonStore { async function parse(filepath: string): Promise> { const file = Bun.file(filepath) if (!(await file.exists())) return {} const text = await file.text() if (!text.trim()) return {} return JSON.parse(text) as Record } /** Read path: a missing, empty, or corrupt file degrades to `{}`. */ export async function read(filepath: string): Promise> { using _ = await Lock.read(filepath) return await parse(filepath).catch(() => ({}) as Record) } /** Write path: refuse to build on a file that exists but cannot be parsed. */ async function load(filepath: string): Promise> { try { return await parse(filepath) } catch (error) { const backup = `${filepath}.corrupt-${process.pid}` await fs.copyFile(filepath, backup).catch(() => {}) const reason = error instanceof Error ? error.message : String(error) throw new Error( `${filepath} exists but could not be parsed (${reason}). ` + `Refusing to overwrite it — that would discard every other entry. ` + `The unmodified file was backed up to ${backup}; repair or remove ${filepath} and retry.`, ) } } async function replace(filepath: string, data: Record) { const temp = `${filepath}.${process.pid}.tmp` await Bun.write(temp, JSON.stringify(data, null, 2), { mode: 0o600 }) try { await fs.rename(temp, filepath) } catch (error) { await fs.unlink(temp).catch(() => {}) throw error } } /** Serialized, atomic read-modify-write. The callback may mutate `data` in * place or return a replacement object. */ export async function update( filepath: string, fn: (data: Record) => Record | void, ): Promise { using _ = await Lock.write(filepath) const data = await load(filepath) const next = fn(data) ?? data await replace(filepath, next) } }