config.ts111 lines · main
1import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2import { homedir } from 'node:os';
3import { dirname, join } from 'node:path';
4
5/**
6 * Where we cache the user's CLI credentials. Follows XDG when set,
7 * otherwise `~/.config/briven/credentials.json`.
8 *
9 * File format is deliberately small — each entry is (projectId, apiKey,
10 * apiOrigin). A future `briven login` that issues per-user personal
11 * access tokens will extend this schema.
12 */
13function configDir(): string {
14 const xdg = process.env.XDG_CONFIG_HOME;
15 const base = xdg && xdg.length > 0 ? xdg : join(homedir(), '.config');
16 return join(base, 'briven');
17}
18
19export function credentialsPath(): string {
20 return join(configDir(), 'credentials.json');
21}
22
23export interface ProjectCredential {
24 projectId: string;
25 apiKey: string;
26 apiOrigin: string;
27 // Last 4 chars of the key, for display.
28 suffix: string;
29 createdAt: string;
30}
31
32export interface UserCredential {
33 token: string;
34 userId: string;
35 apiOrigin: string;
36 savedAt: string;
37}
38
39export interface CredentialsFile {
40 version: 1;
41 default?: string; // projectId
42 projects: Record<string, ProjectCredential>;
43 user?: UserCredential;
44}
45
46const EMPTY: CredentialsFile = { version: 1, projects: {} };
47
48export async function readCredentials(): Promise<CredentialsFile> {
49 try {
50 const raw = await readFile(credentialsPath(), 'utf8');
51 const parsed = JSON.parse(raw) as CredentialsFile;
52 if (!parsed || parsed.version !== 1 || typeof parsed.projects !== 'object') {
53 return EMPTY;
54 }
55 return parsed;
56 } catch (err) {
57 if (isNotFound(err)) return EMPTY;
58 throw err;
59 }
60}
61
62export async function writeCredentials(file: CredentialsFile): Promise<void> {
63 const path = credentialsPath();
64 await mkdir(dirname(path), { recursive: true, mode: 0o700 });
65 await writeFile(path, JSON.stringify(file, null, 2), { mode: 0o600 });
66}
67
68export async function clearCredentials(): Promise<void> {
69 try {
70 await rm(credentialsPath());
71 } catch (err) {
72 if (!isNotFound(err)) throw err;
73 }
74}
75
76export async function writeUserCredential(u: UserCredential): Promise<void> {
77 const current = await readCredentials();
78 await writeCredentials({ ...current, user: u });
79}
80
81export async function readUserCredential(): Promise<UserCredential | null> {
82 const current = await readCredentials();
83 return current.user ?? null;
84}
85
86export async function clearUserCredential(): Promise<void> {
87 const current = await readCredentials();
88 if (!current.user) return;
89 const { user: _drop, ...rest } = current;
90 await writeCredentials(rest);
91}
92
93export async function writeProjectCredential(cred: ProjectCredential): Promise<void> {
94 const current = await readCredentials();
95 const next: CredentialsFile = {
96 ...current,
97 projects: { ...current.projects, [cred.projectId]: cred },
98 };
99 // First project becomes the default so `briven dev` without --project works.
100 if (!current.default) next.default = cred.projectId;
101 await writeCredentials(next);
102}
103
104function isNotFound(err: unknown): boolean {
105 return (
106 typeof err === 'object' &&
107 err !== null &&
108 'code' in err &&
109 (err as { code: string }).code === 'ENOENT'
110 );
111}