packages/cli/src/lib/credentials.ts 4.3 K raw
1
import * as fs from "fs/promises";
2
import * as path from "path";
3
import * as os from "os";
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
}