import * as fs from 'fs/promises' import * as os from 'os' import * as path from 'path' import { z } from 'zod' const rawConfigSchema = z.object({ authToken: z.string().min(1).optional(), baseUrl: z.string().url().optional(), }) export interface Config { authToken?: string baseUrl?: string } function configDir(): string { if (process.platform === 'win32' && process.env.APPDATA) { return path.join(process.env.APPDATA, 'drawcall-market') } const base = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), '.config') return path.join(base, 'drawcall-market') } function configPath(): string { return path.join(configDir(), 'config.json') } export async function loadConfig(): Promise { try { return rawConfigSchema.parse(JSON.parse(await fs.readFile(configPath(), 'utf-8'))) } catch (error) { if (isMissingFile(error)) return null throw error } } export async function saveConfig(config: Config): Promise { const dir = configDir() await fs.mkdir(dir, { recursive: true }) // `mode` on writeFile/mkdir only applies when the path is created, so chmod // explicitly to protect a pre-existing (possibly world-readable) token file // and its directory. A chmod failure means we could not secure the token, so // let it surface rather than silently leaving it exposed. await fs.chmod(dir, 0o700) const file = configPath() await fs.writeFile(file, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 }) await fs.chmod(file, 0o600) } export async function clearConfig(): Promise { try { await fs.unlink(configPath()) return true } catch (error) { if (isMissingFile(error)) return false throw error } } export function getConfigPath(): string { return configPath() } function isMissingFile(error: unknown): boolean { return error instanceof Error && 'code' in error && error.code === 'ENOENT' }