forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
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}