packages/cli/src/lib/credentials.ts 4.1 K raw
1
import * as path from "path";
2
import * as os from "os";
3
import type { Credentials } from "./types";
4
5
const CONFIG_DIR = path.join(os.homedir(), ".config", "sequoia");
6
const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
7
8
// Stored credentials keyed by identifier
9
type CredentialsStore = Record<string, Credentials>;
10
11
/**
12
 * Load all stored credentials
13
 */
14
async 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
 */
39
async 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
 */
55
export 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
 */
101
export 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
 */
111
export 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
 */
119
export 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
 */
128
export 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
157
export function getCredentialsPath(): string {
158
  return CREDENTIALS_FILE;
159
}