A CLI for publishing standard.site documents to ATProto
at v0.1.0 159 lines 4.2 kB view raw
1import * as path from "path"; 2import * as os from "os"; 3import type { Credentials } from "./types"; 4 5const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia"); 6const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json"); 7 8// Stored credentials keyed by identifier 9type CredentialsStore = Record<string, Credentials>; 10 11/** 12 * Load all stored credentials 13 */ 14async function loadCredentialsStore(): Promise<CredentialsStore> { 15 const file = Bun.file(CREDENTIALS_FILE); 16 if (!(await file.exists())) { 17 return {}; 18 } 19 20 try { 21 const content = await file.text(); 22 const parsed = JSON.parse(content); 23 24 // Handle legacy single-credential format (migrate on read) 25 if (parsed.identifier && parsed.password) { 26 const legacy = parsed as Credentials; 27 return { [legacy.identifier]: legacy }; 28 } 29 30 return parsed as CredentialsStore; 31 } catch { 32 return {}; 33 } 34} 35 36/** 37 * Save the entire credentials store 38 */ 39async function saveCredentialsStore(store: CredentialsStore): Promise<void> { 40 await Bun.$`mkdir -p ${CONFIG_DIR}`; 41 await Bun.write(CREDENTIALS_FILE, JSON.stringify(store, null, 2)); 42 await Bun.$`chmod 600 ${CREDENTIALS_FILE}`; 43} 44 45/** 46 * Load credentials for a specific identity or resolve which to use. 47 * 48 * Priority: 49 * 1. Full env vars (ATP_IDENTIFIER + ATP_APP_PASSWORD) 50 * 2. SEQUOIA_PROFILE env var - selects from stored credentials 51 * 3. projectIdentity parameter (from sequoia.json) 52 * 4. If only one identity stored, use it 53 * 5. Return null (caller should prompt user) 54 */ 55export async function loadCredentials( 56 projectIdentity?: string 57): Promise<Credentials | null> { 58 // 1. Check environment variables first (full override) 59 const envIdentifier = process.env.ATP_IDENTIFIER; 60 const envPassword = process.env.ATP_APP_PASSWORD; 61 const envPdsUrl = process.env.PDS_URL; 62 63 if (envIdentifier && envPassword) { 64 return { 65 identifier: envIdentifier, 66 password: envPassword, 67 pdsUrl: envPdsUrl || "https://bsky.social", 68 }; 69 } 70 71 const store = await loadCredentialsStore(); 72 const identifiers = Object.keys(store); 73 74 if (identifiers.length === 0) { 75 return null; 76 } 77 78 // 2. SEQUOIA_PROFILE env var 79 const profileEnv = process.env.SEQUOIA_PROFILE; 80 if (profileEnv && store[profileEnv]) { 81 return store[profileEnv]; 82 } 83 84 // 3. Project-specific identity (from sequoia.json) 85 if (projectIdentity && store[projectIdentity]) { 86 return store[projectIdentity]; 87 } 88 89 // 4. If only one identity, use it 90 if (identifiers.length === 1 && identifiers[0]) { 91 return store[identifiers[0]] ?? null; 92 } 93 94 // Multiple identities exist but none selected 95 return null; 96} 97 98/** 99 * Get a specific identity by identifier 100 */ 101export async function getCredentials( 102 identifier: string 103): Promise<Credentials | null> { 104 const store = await loadCredentialsStore(); 105 return store[identifier] || null; 106} 107 108/** 109 * List all stored identities 110 */ 111export async function listCredentials(): Promise<string[]> { 112 const store = await loadCredentialsStore(); 113 return Object.keys(store); 114} 115 116/** 117 * Save credentials for an identity (adds or updates) 118 */ 119export async function saveCredentials(credentials: Credentials): Promise<void> { 120 const store = await loadCredentialsStore(); 121 store[credentials.identifier] = credentials; 122 await saveCredentialsStore(store); 123} 124 125/** 126 * Delete credentials for a specific identity 127 */ 128export async function deleteCredentials(identifier?: string): Promise<boolean> { 129 const store = await loadCredentialsStore(); 130 const identifiers = Object.keys(store); 131 132 if (identifiers.length === 0) { 133 return false; 134 } 135 136 // If identifier specified, delete just that one 137 if (identifier) { 138 if (!store[identifier]) { 139 return false; 140 } 141 delete store[identifier]; 142 await saveCredentialsStore(store); 143 return true; 144 } 145 146 // If only one identity, delete it (backwards compat behavior) 147 if (identifiers.length === 1 && identifiers[0]) { 148 delete store[identifiers[0]]; 149 await saveCredentialsStore(store); 150 return true; 151 } 152 153 // Multiple identities but none specified 154 return false; 155} 156 157export function getCredentialsPath(): string { 158 return CREDENTIALS_FILE; 159}