1import { join } from "@std/path";2import { fail } from "./fmt.ts";34export interface Config {5 server: string;6 token: string;7 defaultVault?: string;8 /**9 * Optional allow-list of vault names this config may target by name. When10 * present, any push/share/rm/revert/default that names a vault outside the11 * list is refused — so a restricted agent can't drop content into a vault12 * that sits adjacent to things you share professionally. Absent = no limit;13 * empty array = no named vault is permitted.14 */15 allowVaults?: string[];16}1718/** Throw if `config.allowVaults` is set and does not include `vault`. */19export function assertVaultAllowed(vault: string, config: Config): void {20 if (!config.allowVaults) return; // unset → unrestricted21 if (!config.allowVaults.includes(vault)) {22 const allowed = config.allowVaults.length ? config.allowVaults.join(", ") : "none";23 throw new Error(`vault "${vault}" is not allowed by this config (allowed: ${allowed})`);24 }25}2627export function configDir(): string {28 const xdg = Deno.env.get("XDG_CONFIG_HOME");29 const home = Deno.env.get("HOME") ?? ".";30 return join(xdg ?? join(home, ".config"), "nzip");31}3233export function configPath(): string {34 return join(configDir(), "config.json");35}3637export async function loadConfig(): Promise<Config | null> {38 try {39 return JSON.parse(await Deno.readTextFile(configPath())) as Config;40 } catch {41 return null;42 }43}4445export async function saveConfig(config: Config): Promise<void> {46 await Deno.mkdir(configDir(), { recursive: true });47 const path = configPath();48 await Deno.writeTextFile(path, JSON.stringify(config, null, 2) + "\n");49 await Deno.chmod(path, 0o600);50}5152export async function requireConfig(): Promise<Config> {53 const config = await loadConfig();54 if (!config) fail("not authenticated");55 return config;56}