config.ts111 lines · main
| 1 | import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; |
| 2 | import { homedir } from 'node:os'; |
| 3 | import { 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 | */ |
| 13 | function 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 | |
| 19 | export function credentialsPath(): string { |
| 20 | return join(configDir(), 'credentials.json'); |
| 21 | } |
| 22 | |
| 23 | export 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 | |
| 32 | export interface UserCredential { |
| 33 | token: string; |
| 34 | userId: string; |
| 35 | apiOrigin: string; |
| 36 | savedAt: string; |
| 37 | } |
| 38 | |
| 39 | export interface CredentialsFile { |
| 40 | version: 1; |
| 41 | default?: string; // projectId |
| 42 | projects: Record<string, ProjectCredential>; |
| 43 | user?: UserCredential; |
| 44 | } |
| 45 | |
| 46 | const EMPTY: CredentialsFile = { version: 1, projects: {} }; |
| 47 | |
| 48 | export 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 | |
| 62 | export 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 | |
| 68 | export async function clearCredentials(): Promise<void> { |
| 69 | try { |
| 70 | await rm(credentialsPath()); |
| 71 | } catch (err) { |
| 72 | if (!isNotFound(err)) throw err; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | export async function writeUserCredential(u: UserCredential): Promise<void> { |
| 77 | const current = await readCredentials(); |
| 78 | await writeCredentials({ ...current, user: u }); |
| 79 | } |
| 80 | |
| 81 | export async function readUserCredential(): Promise<UserCredential | null> { |
| 82 | const current = await readCredentials(); |
| 83 | return current.user ?? null; |
| 84 | } |
| 85 | |
| 86 | export 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 | |
| 93 | export 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 | |
| 104 | function 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 | } |