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