packages/cli/src/lib/credentials.ts 4.2 K raw
1
import * as fs from "node:fs/promises";
2
import * as os from "node:os";
3
import * as path from "node:path";
4
import type { Credentials } from "./types";
5
6
const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia");
7
const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
8
9
// Stored credentials keyed by identifier
10
type CredentialsStore = Record<string, Credentials>;
11
12
async 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
 */
24
async 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
 */
48
async 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
 */
64
export 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
 */
110
export 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
 */
120
export 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
 */
128
export 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
 */
137
export 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
166
export function getCredentialsPath(): string {
167
	return CREDENTIALS_FILE;
168
}