@bjesuiter/codex-switcher 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -44
- package/cdx.mjs +1094 -111
- package/package.json +3 -1
package/cdx.mjs
CHANGED
|
@@ -1,43 +1,94 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import tab from "@bomb.sh/tab/commander";
|
|
3
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
6
|
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { deletePassword, getPassword, listBackends, setPassword, useBackend } from "@bjesuiter/cross-keychain";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
9
12
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
10
13
|
import { randomBytes } from "node:crypto";
|
|
11
14
|
import http from "node:http";
|
|
12
15
|
|
|
13
16
|
//#region package.json
|
|
14
|
-
var version = "1.
|
|
17
|
+
var version = "1.5.0";
|
|
15
18
|
|
|
16
19
|
//#endregion
|
|
17
|
-
//#region lib/
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
//#region lib/platform/path-resolver.ts
|
|
21
|
+
const envValue = (env, key) => {
|
|
22
|
+
const value = env[key];
|
|
23
|
+
if (!value) return void 0;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
26
|
+
};
|
|
27
|
+
const resolvePiAuthPath = (env, homeDir, platform) => {
|
|
28
|
+
const piAgentDir = envValue(env, "PI_CODING_AGENT_DIR");
|
|
29
|
+
if (piAgentDir) return platform === "win32" ? path.win32.join(piAgentDir, "auth.json") : path.join(piAgentDir, "auth.json");
|
|
30
|
+
return platform === "win32" ? path.win32.join(homeDir, ".pi", "agent", "auth.json") : path.join(homeDir, ".pi", "agent", "auth.json");
|
|
31
|
+
};
|
|
32
|
+
const resolveXdgPaths = (env, homeDir, platform) => {
|
|
33
|
+
const configHome = envValue(env, "XDG_CONFIG_HOME") ?? path.join(homeDir, ".config");
|
|
34
|
+
const dataHome = envValue(env, "XDG_DATA_HOME") ?? path.join(homeDir, ".local", "share");
|
|
35
|
+
const configDir = path.join(configHome, "cdx");
|
|
36
|
+
return {
|
|
37
|
+
profile: "xdg",
|
|
38
|
+
configDir,
|
|
39
|
+
configPath: path.join(configDir, "accounts.json"),
|
|
40
|
+
authPath: path.join(dataHome, "opencode", "auth.json"),
|
|
41
|
+
codexAuthPath: path.join(homeDir, ".codex", "auth.json"),
|
|
42
|
+
piAuthPath: resolvePiAuthPath(env, homeDir, platform)
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const resolveWindowsPaths = (env, homeDir) => {
|
|
46
|
+
const winPath = path.win32;
|
|
47
|
+
const appData = envValue(env, "APPDATA") ?? winPath.join(homeDir, "AppData", "Roaming");
|
|
48
|
+
const localAppData = envValue(env, "LOCALAPPDATA") ?? winPath.join(homeDir, "AppData", "Local");
|
|
49
|
+
const configDir = winPath.join(appData, "cdx");
|
|
50
|
+
return {
|
|
51
|
+
profile: "windows-appdata",
|
|
52
|
+
configDir,
|
|
53
|
+
configPath: winPath.join(configDir, "accounts.json"),
|
|
54
|
+
authPath: winPath.join(localAppData, "opencode", "auth.json"),
|
|
55
|
+
codexAuthPath: winPath.join(homeDir, ".codex", "auth.json"),
|
|
56
|
+
piAuthPath: resolvePiAuthPath(env, homeDir, "win32")
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
const resolveRuntimePaths = (input) => {
|
|
60
|
+
if (input.platform === "win32") return resolveWindowsPaths(input.env, input.homeDir);
|
|
61
|
+
return resolveXdgPaths(input.env, input.homeDir, input.platform);
|
|
22
62
|
};
|
|
23
63
|
|
|
24
64
|
//#endregion
|
|
25
65
|
//#region lib/paths.ts
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const createDefaultPaths = () => ({
|
|
33
|
-
configDir: defaultConfigDir,
|
|
34
|
-
configPath: path.join(defaultConfigDir, "accounts.json"),
|
|
35
|
-
authPath: path.join(os.homedir(), ".local", "share", "opencode", "auth.json"),
|
|
36
|
-
codexAuthPath: path.join(os.homedir(), ".codex", "auth.json"),
|
|
37
|
-
piAuthPath: resolvePiAuthPath()
|
|
66
|
+
const toPathConfig = (paths) => ({
|
|
67
|
+
configDir: paths.configDir,
|
|
68
|
+
configPath: paths.configPath,
|
|
69
|
+
authPath: paths.authPath,
|
|
70
|
+
codexAuthPath: paths.codexAuthPath,
|
|
71
|
+
piAuthPath: paths.piAuthPath
|
|
38
72
|
});
|
|
39
|
-
|
|
73
|
+
const createDefaultPaths = () => {
|
|
74
|
+
const resolved = resolveRuntimePaths({
|
|
75
|
+
platform: process.platform,
|
|
76
|
+
env: process.env,
|
|
77
|
+
homeDir: os.homedir()
|
|
78
|
+
});
|
|
79
|
+
return {
|
|
80
|
+
paths: toPathConfig(resolved),
|
|
81
|
+
resolution: {
|
|
82
|
+
platform: process.platform,
|
|
83
|
+
profile: resolved.profile
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
const initial = createDefaultPaths();
|
|
88
|
+
let currentPaths = initial.paths;
|
|
89
|
+
let currentResolution = initial.resolution;
|
|
40
90
|
const getPaths = () => currentPaths;
|
|
91
|
+
const getPathResolutionInfo = () => currentResolution;
|
|
41
92
|
const setPaths = (paths) => {
|
|
42
93
|
currentPaths = {
|
|
43
94
|
...currentPaths,
|
|
@@ -46,7 +97,9 @@ const setPaths = (paths) => {
|
|
|
46
97
|
if (paths.configDir && !paths.configPath) currentPaths.configPath = path.join(paths.configDir, "accounts.json");
|
|
47
98
|
};
|
|
48
99
|
const resetPaths = () => {
|
|
49
|
-
|
|
100
|
+
const next = createDefaultPaths();
|
|
101
|
+
currentPaths = next.paths;
|
|
102
|
+
currentResolution = next.resolution;
|
|
50
103
|
};
|
|
51
104
|
const createTestPaths = (testDir) => ({
|
|
52
105
|
configDir: path.join(testDir, "config"),
|
|
@@ -56,6 +109,48 @@ const createTestPaths = (testDir) => ({
|
|
|
56
109
|
piAuthPath: path.join(testDir, "pi", "auth.json")
|
|
57
110
|
});
|
|
58
111
|
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region lib/config.ts
|
|
114
|
+
const isSecretStoreSelection = (value) => value === "auto" || value === "legacy-keychain";
|
|
115
|
+
const loadConfiguredSecretStoreSelection = async () => {
|
|
116
|
+
const { configPath } = getPaths();
|
|
117
|
+
if (!existsSync(configPath)) return;
|
|
118
|
+
try {
|
|
119
|
+
const raw = await readFile(configPath, "utf8");
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
return isSecretStoreSelection(parsed.secretStore) ? parsed.secretStore : void 0;
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const loadConfig = async () => {
|
|
127
|
+
const { configPath } = getPaths();
|
|
128
|
+
if (!existsSync(configPath)) throw new Error(`Missing config at ${configPath}. Create accounts.json to list Keychain services.`);
|
|
129
|
+
const raw = await readFile(configPath, "utf8");
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (!Array.isArray(parsed.accounts) || parsed.accounts.length === 0) throw new Error("accounts.json must include a non-empty accounts array.");
|
|
132
|
+
if (typeof parsed.current !== "number" || Number.isNaN(parsed.current)) parsed.current = 0;
|
|
133
|
+
if (!isSecretStoreSelection(parsed.secretStore)) delete parsed.secretStore;
|
|
134
|
+
return parsed;
|
|
135
|
+
};
|
|
136
|
+
const saveConfig = async (config) => {
|
|
137
|
+
const { configDir, configPath } = getPaths();
|
|
138
|
+
await mkdir(configDir, { recursive: true });
|
|
139
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
140
|
+
};
|
|
141
|
+
const configExists = () => {
|
|
142
|
+
const { configPath } = getPaths();
|
|
143
|
+
return existsSync(configPath);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region lib/commands/errors.ts
|
|
148
|
+
const exitWithCommandError = (error) => {
|
|
149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
+
process.stderr.write(`${message}\n`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
};
|
|
153
|
+
|
|
59
154
|
//#endregion
|
|
60
155
|
//#region lib/auth.ts
|
|
61
156
|
const readExistingJson = async (filePath) => {
|
|
@@ -137,31 +232,70 @@ const writeAllAuthFiles = async (payload) => {
|
|
|
137
232
|
};
|
|
138
233
|
|
|
139
234
|
//#endregion
|
|
140
|
-
//#region lib/
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
235
|
+
//#region lib/platform/browser.ts
|
|
236
|
+
const getBrowserLauncher = (platform = process.platform, url) => {
|
|
237
|
+
if (platform === "darwin") return {
|
|
238
|
+
command: "open",
|
|
239
|
+
args: [url],
|
|
240
|
+
label: "open"
|
|
241
|
+
};
|
|
242
|
+
if (platform === "win32") return {
|
|
243
|
+
command: "cmd",
|
|
244
|
+
args: [
|
|
245
|
+
"/c",
|
|
246
|
+
"start",
|
|
247
|
+
"",
|
|
248
|
+
url
|
|
249
|
+
],
|
|
250
|
+
label: "cmd /c start"
|
|
251
|
+
};
|
|
252
|
+
return {
|
|
253
|
+
command: "xdg-open",
|
|
254
|
+
args: [url],
|
|
255
|
+
label: "xdg-open"
|
|
256
|
+
};
|
|
149
257
|
};
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
258
|
+
const isCommandAvailable = (command, platform = process.platform) => {
|
|
259
|
+
const probe = platform === "win32" ? "where" : "which";
|
|
260
|
+
return Bun.spawnSync({
|
|
261
|
+
cmd: [probe, command],
|
|
262
|
+
stdout: "pipe",
|
|
263
|
+
stderr: "pipe"
|
|
264
|
+
}).exitCode === 0;
|
|
154
265
|
};
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
return
|
|
266
|
+
const getBrowserLauncherCapability = (platform = process.platform) => {
|
|
267
|
+
const launcher = getBrowserLauncher(platform, "https://example.com");
|
|
268
|
+
return {
|
|
269
|
+
command: launcher.command,
|
|
270
|
+
label: launcher.label,
|
|
271
|
+
available: isCommandAvailable(launcher.command, platform)
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
const openBrowserUrl = (url, spawnImpl = spawn) => {
|
|
275
|
+
const launcher = getBrowserLauncher(process.platform, url);
|
|
276
|
+
try {
|
|
277
|
+
spawnImpl(launcher.command, launcher.args, {
|
|
278
|
+
detached: true,
|
|
279
|
+
stdio: "ignore"
|
|
280
|
+
}).unref();
|
|
281
|
+
return {
|
|
282
|
+
ok: true,
|
|
283
|
+
launcher
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
launcher,
|
|
289
|
+
error: error instanceof Error ? error.message : String(error)
|
|
290
|
+
};
|
|
291
|
+
}
|
|
158
292
|
};
|
|
159
293
|
|
|
160
294
|
//#endregion
|
|
161
295
|
//#region lib/keychain.ts
|
|
162
|
-
const SERVICE_PREFIX = "cdx-openai-";
|
|
296
|
+
const SERVICE_PREFIX$3 = "cdx-openai-";
|
|
163
297
|
const getKeychainService = (accountId) => {
|
|
164
|
-
return `${SERVICE_PREFIX}${accountId}`;
|
|
298
|
+
return `${SERVICE_PREFIX$3}${accountId}`;
|
|
165
299
|
};
|
|
166
300
|
const runSecurity = (args) => {
|
|
167
301
|
const result = Bun.spawnSync({
|
|
@@ -186,6 +320,23 @@ const runSecuritySafe = (args) => {
|
|
|
186
320
|
output: result.exitCode === 0 ? result.stdout.toString() : result.stderr.toString()
|
|
187
321
|
};
|
|
188
322
|
};
|
|
323
|
+
const runSecuritySafeAsync = async (args) => {
|
|
324
|
+
const childProcess = Bun.spawn(["security", ...args], {
|
|
325
|
+
stderr: "pipe",
|
|
326
|
+
stdout: "pipe"
|
|
327
|
+
});
|
|
328
|
+
const stdoutPromise = childProcess.stdout ? new Response(childProcess.stdout).text() : Promise.resolve("");
|
|
329
|
+
const stderrPromise = childProcess.stderr ? new Response(childProcess.stderr).text() : Promise.resolve("");
|
|
330
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
331
|
+
childProcess.exited,
|
|
332
|
+
stdoutPromise,
|
|
333
|
+
stderrPromise
|
|
334
|
+
]);
|
|
335
|
+
return {
|
|
336
|
+
success: exitCode === 0,
|
|
337
|
+
output: exitCode === 0 ? stdout : stderr
|
|
338
|
+
};
|
|
339
|
+
};
|
|
189
340
|
const saveKeychainPayload = (accountId, payload) => {
|
|
190
341
|
runSecurity([
|
|
191
342
|
"add-generic-password",
|
|
@@ -233,12 +384,492 @@ const listKeychainAccounts = () => {
|
|
|
233
384
|
if (result.exitCode !== 0) return [];
|
|
234
385
|
const output = result.stdout.toString();
|
|
235
386
|
const accounts = [];
|
|
236
|
-
const serviceRegex = new RegExp(`"svce"<blob>="${SERVICE_PREFIX}([^"]+)"`, "g");
|
|
387
|
+
const serviceRegex = new RegExp(`"svce"<blob>="${SERVICE_PREFIX$3}([^"]+)"`, "g");
|
|
237
388
|
let match;
|
|
238
389
|
while ((match = serviceRegex.exec(output)) !== null) if (match[1]) accounts.push(match[1]);
|
|
239
390
|
return [...new Set(accounts)];
|
|
240
391
|
};
|
|
241
392
|
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region lib/secrets/cross-keychain-overrides.ts
|
|
395
|
+
const LEGACY_MAX_PASSWORD_LENGTH = 4096;
|
|
396
|
+
const DEFAULT_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH = 16384;
|
|
397
|
+
const parseMaxPasswordLength = (value) => {
|
|
398
|
+
if (!value) return null;
|
|
399
|
+
const parsed = Number.parseInt(value, 10);
|
|
400
|
+
if (!Number.isInteger(parsed) || parsed <= LEGACY_MAX_PASSWORD_LENGTH) return null;
|
|
401
|
+
return parsed;
|
|
402
|
+
};
|
|
403
|
+
const getCrossKeychainBackendOverrides = () => {
|
|
404
|
+
return { max_password_length: parseMaxPasswordLength(process.env.CDX_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH) ?? DEFAULT_CROSS_KEYCHAIN_MAX_PASSWORD_LENGTH };
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region lib/secrets/fallback-consent.ts
|
|
409
|
+
const CONSENT_FILE = "secure-store-fallback-consent.json";
|
|
410
|
+
const CONSENT_ENV_BYPASS = "CDX_ALLOW_SECURE_STORE_FALLBACK";
|
|
411
|
+
const isBypassEnabled = () => {
|
|
412
|
+
const value = process.env[CONSENT_ENV_BYPASS];
|
|
413
|
+
if (!value) return false;
|
|
414
|
+
return [
|
|
415
|
+
"1",
|
|
416
|
+
"true",
|
|
417
|
+
"yes",
|
|
418
|
+
"y"
|
|
419
|
+
].includes(value.trim().toLowerCase());
|
|
420
|
+
};
|
|
421
|
+
const consentFilePath = () => path.join(getPaths().configDir, CONSENT_FILE);
|
|
422
|
+
const loadConsentMap = async () => {
|
|
423
|
+
try {
|
|
424
|
+
const raw = await readFile(consentFilePath(), "utf8");
|
|
425
|
+
return JSON.parse(raw).accepted ?? {};
|
|
426
|
+
} catch {
|
|
427
|
+
return {};
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const saveConsentMap = async (accepted) => {
|
|
431
|
+
const { configDir } = getPaths();
|
|
432
|
+
await mkdir(configDir, { recursive: true });
|
|
433
|
+
const payload = { accepted };
|
|
434
|
+
await writeFile(consentFilePath(), JSON.stringify(payload, null, 2), "utf8");
|
|
435
|
+
};
|
|
436
|
+
const promptConsent = async (message) => {
|
|
437
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
|
|
438
|
+
process.stdout.write(`\n${message}\n\n`);
|
|
439
|
+
const rl = createInterface({
|
|
440
|
+
input: process.stdin,
|
|
441
|
+
output: process.stdout
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
const normalized = (await rl.question("Do you want to continue with this fallback? [y/N]: ")).trim().toLowerCase();
|
|
445
|
+
return normalized === "y" || normalized === "yes";
|
|
446
|
+
} finally {
|
|
447
|
+
rl.close();
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
const ensureFallbackConsent = async (scope, warningMessage) => {
|
|
451
|
+
if (isBypassEnabled()) return;
|
|
452
|
+
const accepted = await loadConsentMap();
|
|
453
|
+
if (accepted[scope]) return;
|
|
454
|
+
if (!await promptConsent(warningMessage)) throw new Error(`Secure-store fallback usage was not approved for '${scope}'. Re-run in an interactive terminal to confirm, or set ${CONSENT_ENV_BYPASS}=1 if you accept the fallback risk.`);
|
|
455
|
+
accepted[scope] = { acceptedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
456
|
+
await saveConsentMap(accepted);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region lib/secrets/linux-cross-keychain.ts
|
|
461
|
+
const SERVICE_PREFIX$2 = "cdx-openai-";
|
|
462
|
+
const LINUX_FALLBACK_SCOPE = "linux:cross-keychain:secret-service";
|
|
463
|
+
let backendInitPromise$2 = null;
|
|
464
|
+
let selectedBackend$2 = null;
|
|
465
|
+
const tryUseBackend$2 = async (backendId) => {
|
|
466
|
+
try {
|
|
467
|
+
await useBackend(backendId, getCrossKeychainBackendOverrides());
|
|
468
|
+
return true;
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const selectBackend$2 = async () => {
|
|
474
|
+
const backends = await listBackends();
|
|
475
|
+
const available = new Set(backends.map((backend) => backend.id));
|
|
476
|
+
if (available.has("native-linux") && await tryUseBackend$2("native-linux")) return "native-linux";
|
|
477
|
+
if (available.has("secret-service") && await tryUseBackend$2("secret-service")) return "secret-service";
|
|
478
|
+
if (await tryUseBackend$2("native-linux")) return "native-linux";
|
|
479
|
+
if (await tryUseBackend$2("secret-service")) return "secret-service";
|
|
480
|
+
throw new Error("Unable to initialize Linux secure-store backend via cross-keychain.");
|
|
481
|
+
};
|
|
482
|
+
const ensureLinuxBackend = async (options = {}) => {
|
|
483
|
+
if (!backendInitPromise$2) backendInitPromise$2 = (async () => {
|
|
484
|
+
selectedBackend$2 = await selectBackend$2();
|
|
485
|
+
})();
|
|
486
|
+
try {
|
|
487
|
+
await backendInitPromise$2;
|
|
488
|
+
} catch {
|
|
489
|
+
backendInitPromise$2 = null;
|
|
490
|
+
selectedBackend$2 = null;
|
|
491
|
+
throw new Error("Unable to initialize Linux secure-store backend via cross-keychain.");
|
|
492
|
+
}
|
|
493
|
+
if (options.forWrite && selectedBackend$2 === "secret-service") await ensureFallbackConsent(LINUX_FALLBACK_SCOPE, "⚠ Security warning: only the cross-keychain Linux fallback backend is available.\nThis path relies on shell-based `secret-tool` operations for Secret Service access.\nCompared to native bindings, secrets may be more exposed to process inspection/logging while helper commands run.");
|
|
494
|
+
};
|
|
495
|
+
const getLinuxCrossKeychainService = (accountId) => `${SERVICE_PREFIX$2}${accountId}`;
|
|
496
|
+
const parsePayload$2 = (accountId, raw) => {
|
|
497
|
+
let parsed;
|
|
498
|
+
try {
|
|
499
|
+
parsed = JSON.parse(raw);
|
|
500
|
+
} catch {
|
|
501
|
+
throw new Error(`Stored credential payload for account ${accountId} is not valid JSON.`);
|
|
502
|
+
}
|
|
503
|
+
if (!parsed.refresh || !parsed.access || !parsed.expires || !parsed.accountId) throw new Error(`Stored credential payload for account ${accountId} is missing required fields.`);
|
|
504
|
+
return parsed;
|
|
505
|
+
};
|
|
506
|
+
const withService$2 = async (accountId, run, options = {}) => {
|
|
507
|
+
await ensureLinuxBackend(options);
|
|
508
|
+
return run(getLinuxCrossKeychainService(accountId));
|
|
509
|
+
};
|
|
510
|
+
const saveLinuxCrossKeychainPayload = async (accountId, payload) => withService$2(accountId, (service) => setPassword(service, accountId, JSON.stringify(payload)), { forWrite: true });
|
|
511
|
+
const loadLinuxCrossKeychainPayload = async (accountId) => {
|
|
512
|
+
const raw = await withService$2(accountId, (service) => getPassword(service, accountId));
|
|
513
|
+
if (raw === null) throw new Error(`No stored credentials found for account ${accountId}.`);
|
|
514
|
+
return parsePayload$2(accountId, raw);
|
|
515
|
+
};
|
|
516
|
+
const deleteLinuxCrossKeychainPayload = async (accountId) => withService$2(accountId, (service) => deletePassword(service, accountId));
|
|
517
|
+
const linuxCrossKeychainPayloadExists = async (accountId) => withService$2(accountId, async (service) => await getPassword(service, accountId) !== null);
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region lib/secrets/macos-cross-keychain.ts
|
|
521
|
+
const SERVICE_PREFIX$1 = "cdx-openai-";
|
|
522
|
+
const MACOS_FALLBACK_SCOPE = "darwin:cross-keychain:macos";
|
|
523
|
+
let backendInitPromise$1 = null;
|
|
524
|
+
let selectedBackend$1 = null;
|
|
525
|
+
const tryUseBackend$1 = async (backendId) => {
|
|
526
|
+
try {
|
|
527
|
+
await useBackend(backendId, getCrossKeychainBackendOverrides());
|
|
528
|
+
return true;
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
const selectBackend$1 = async () => {
|
|
534
|
+
const backends = await listBackends();
|
|
535
|
+
const available = new Set(backends.map((backend) => backend.id));
|
|
536
|
+
if (available.has("native-macos") && await tryUseBackend$1("native-macos")) return "native-macos";
|
|
537
|
+
if (available.has("macos") && await tryUseBackend$1("macos")) return "macos";
|
|
538
|
+
if (await tryUseBackend$1("native-macos")) return "native-macos";
|
|
539
|
+
if (await tryUseBackend$1("macos")) return "macos";
|
|
540
|
+
throw new Error("Unable to initialize macOS keychain backend via cross-keychain.");
|
|
541
|
+
};
|
|
542
|
+
const ensureMacOSBackend = async (options = {}) => {
|
|
543
|
+
if (!backendInitPromise$1) backendInitPromise$1 = (async () => {
|
|
544
|
+
selectedBackend$1 = await selectBackend$1();
|
|
545
|
+
})();
|
|
546
|
+
try {
|
|
547
|
+
await backendInitPromise$1;
|
|
548
|
+
} catch {
|
|
549
|
+
backendInitPromise$1 = null;
|
|
550
|
+
selectedBackend$1 = null;
|
|
551
|
+
throw new Error("Unable to initialize macOS keychain backend via cross-keychain.");
|
|
552
|
+
}
|
|
553
|
+
if (options.forWrite && selectedBackend$1 === "macos") await ensureFallbackConsent(MACOS_FALLBACK_SCOPE, "⚠ Security warning: only the cross-keychain macOS fallback backend is available.\nThis path uses the `security` command to access Keychain.\nCompared to native bindings, secrets may be more exposed to process inspection/logging while helper commands run.");
|
|
554
|
+
};
|
|
555
|
+
const resolveMacOSCrossKeychainBackendId$1 = async () => {
|
|
556
|
+
await ensureMacOSBackend();
|
|
557
|
+
if (!selectedBackend$1) throw new Error("Unable to initialize macOS keychain backend via cross-keychain.");
|
|
558
|
+
return selectedBackend$1;
|
|
559
|
+
};
|
|
560
|
+
const getMacOSCrossKeychainService = (accountId) => `${SERVICE_PREFIX$1}${accountId}`;
|
|
561
|
+
const parsePayload$1 = (accountId, raw) => {
|
|
562
|
+
let parsed;
|
|
563
|
+
try {
|
|
564
|
+
parsed = JSON.parse(raw);
|
|
565
|
+
} catch {
|
|
566
|
+
throw new Error(`Stored credential payload for account ${accountId} is not valid JSON.`);
|
|
567
|
+
}
|
|
568
|
+
if (!parsed.refresh || !parsed.access || !parsed.expires || !parsed.accountId) throw new Error(`Stored credential payload for account ${accountId} is missing required fields.`);
|
|
569
|
+
return parsed;
|
|
570
|
+
};
|
|
571
|
+
const withService$1 = async (accountId, run, options = {}) => {
|
|
572
|
+
await ensureMacOSBackend(options);
|
|
573
|
+
return run(getMacOSCrossKeychainService(accountId));
|
|
574
|
+
};
|
|
575
|
+
const saveMacOSCrossKeychainPayload = async (accountId, payload) => withService$1(accountId, (service) => setPassword(service, accountId, JSON.stringify(payload)), { forWrite: true });
|
|
576
|
+
const loadMacOSCrossKeychainPayload = async (accountId) => {
|
|
577
|
+
const raw = await withService$1(accountId, (service) => getPassword(service, accountId));
|
|
578
|
+
if (raw === null) throw new Error(`No stored credentials found for account ${accountId}.`);
|
|
579
|
+
return parsePayload$1(accountId, raw);
|
|
580
|
+
};
|
|
581
|
+
const deleteMacOSCrossKeychainPayload = async (accountId) => withService$1(accountId, (service) => deletePassword(service, accountId));
|
|
582
|
+
const macosCrossKeychainPayloadExists = async (accountId) => withService$1(accountId, async (service) => await getPassword(service, accountId) !== null);
|
|
583
|
+
|
|
584
|
+
//#endregion
|
|
585
|
+
//#region lib/secrets/windows-cross-keychain.ts
|
|
586
|
+
const SERVICE_PREFIX = "cdx-openai-";
|
|
587
|
+
const WINDOWS_FALLBACK_SCOPE = "win32:cross-keychain:windows";
|
|
588
|
+
let backendInitPromise = null;
|
|
589
|
+
let selectedBackend = null;
|
|
590
|
+
const tryUseBackend = async (backendId) => {
|
|
591
|
+
try {
|
|
592
|
+
await useBackend(backendId, getCrossKeychainBackendOverrides());
|
|
593
|
+
return true;
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
const selectBackend = async () => {
|
|
599
|
+
const backends = await listBackends();
|
|
600
|
+
const available = new Set(backends.map((backend) => backend.id));
|
|
601
|
+
if (available.has("native-windows") && await tryUseBackend("native-windows")) return "native-windows";
|
|
602
|
+
if (available.has("windows") && await tryUseBackend("windows")) return "windows";
|
|
603
|
+
if (await tryUseBackend("native-windows")) return "native-windows";
|
|
604
|
+
if (await tryUseBackend("windows")) return "windows";
|
|
605
|
+
throw new Error("Unable to initialize Windows credential backend via cross-keychain.");
|
|
606
|
+
};
|
|
607
|
+
const ensureWindowsBackend = async (options = {}) => {
|
|
608
|
+
if (!backendInitPromise) backendInitPromise = (async () => {
|
|
609
|
+
selectedBackend = await selectBackend();
|
|
610
|
+
})();
|
|
611
|
+
try {
|
|
612
|
+
await backendInitPromise;
|
|
613
|
+
} catch {
|
|
614
|
+
backendInitPromise = null;
|
|
615
|
+
selectedBackend = null;
|
|
616
|
+
throw new Error("Unable to initialize Windows credential backend via cross-keychain.");
|
|
617
|
+
}
|
|
618
|
+
if (options.forWrite && selectedBackend === "windows") await ensureFallbackConsent(WINDOWS_FALLBACK_SCOPE, "⚠ Security warning: only the cross-keychain Windows fallback backend is available.\nThis path runs a PowerShell helper to access Windows Credential Manager.\nCompared to native bindings, secrets may be more exposed to process inspection/logging while the helper runs.");
|
|
619
|
+
};
|
|
620
|
+
const getWindowsCrossKeychainService = (accountId) => `${SERVICE_PREFIX}${accountId}`;
|
|
621
|
+
const parsePayload = (accountId, raw) => {
|
|
622
|
+
let parsed;
|
|
623
|
+
try {
|
|
624
|
+
parsed = JSON.parse(raw);
|
|
625
|
+
} catch {
|
|
626
|
+
throw new Error(`Stored credential payload for account ${accountId} is not valid JSON.`);
|
|
627
|
+
}
|
|
628
|
+
if (!parsed.refresh || !parsed.access || !parsed.expires || !parsed.accountId) throw new Error(`Stored credential payload for account ${accountId} is missing required fields.`);
|
|
629
|
+
return parsed;
|
|
630
|
+
};
|
|
631
|
+
const withService = async (accountId, run, options = {}) => {
|
|
632
|
+
await ensureWindowsBackend(options);
|
|
633
|
+
return run(getWindowsCrossKeychainService(accountId));
|
|
634
|
+
};
|
|
635
|
+
const saveWindowsCrossKeychainPayload = async (accountId, payload) => withService(accountId, async (service) => {
|
|
636
|
+
await setPassword(service, accountId, JSON.stringify(payload));
|
|
637
|
+
}, { forWrite: true });
|
|
638
|
+
const loadWindowsCrossKeychainPayload = async (accountId) => {
|
|
639
|
+
const raw = await withService(accountId, (service) => getPassword(service, accountId));
|
|
640
|
+
if (raw === null) throw new Error(`No stored credentials found for account ${accountId}.`);
|
|
641
|
+
return parsePayload(accountId, raw);
|
|
642
|
+
};
|
|
643
|
+
const deleteWindowsCrossKeychainPayload = async (accountId) => withService(accountId, async (service) => {
|
|
644
|
+
await deletePassword(service, accountId);
|
|
645
|
+
});
|
|
646
|
+
const windowsCrossKeychainPayloadExists = async (accountId) => withService(accountId, async (service) => await getPassword(service, accountId) !== null);
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region lib/secrets/store.ts
|
|
650
|
+
const MISSING_SECRET_STORE_ERROR_MARKERS = [
|
|
651
|
+
"No stored credentials found",
|
|
652
|
+
"No Keychain payload found",
|
|
653
|
+
"Password not found"
|
|
654
|
+
];
|
|
655
|
+
const isMissingSecretStoreEntryError = (error) => {
|
|
656
|
+
if (!(error instanceof Error)) return false;
|
|
657
|
+
return MISSING_SECRET_STORE_ERROR_MARKERS.some((marker) => error.message.includes(marker));
|
|
658
|
+
};
|
|
659
|
+
const createMissingSecretStoreEntryError = (accountId) => /* @__PURE__ */ new Error(`No stored credentials found for account ${accountId}.`);
|
|
660
|
+
const CACHED_ADAPTER_SYMBOL = Symbol.for("cdx.secretStore.cachedAdapter");
|
|
661
|
+
const withSecretStoreCache = (adapter) => {
|
|
662
|
+
if (adapter[CACHED_ADAPTER_SYMBOL]) return adapter;
|
|
663
|
+
const payloadCache = /* @__PURE__ */ new Map();
|
|
664
|
+
const existsCache = /* @__PURE__ */ new Map();
|
|
665
|
+
const missingAccounts = /* @__PURE__ */ new Set();
|
|
666
|
+
const inFlightLoads = /* @__PURE__ */ new Map();
|
|
667
|
+
const markPresent = (accountId, payload) => {
|
|
668
|
+
payloadCache.set(accountId, payload);
|
|
669
|
+
existsCache.set(accountId, true);
|
|
670
|
+
missingAccounts.delete(accountId);
|
|
671
|
+
};
|
|
672
|
+
const markMissing = (accountId) => {
|
|
673
|
+
payloadCache.delete(accountId);
|
|
674
|
+
existsCache.set(accountId, false);
|
|
675
|
+
missingAccounts.add(accountId);
|
|
676
|
+
};
|
|
677
|
+
const loadAndCache = async (accountId) => {
|
|
678
|
+
const existingPromise = inFlightLoads.get(accountId);
|
|
679
|
+
if (existingPromise) return existingPromise;
|
|
680
|
+
const promise = (async () => {
|
|
681
|
+
try {
|
|
682
|
+
const payload = await adapter.load(accountId);
|
|
683
|
+
markPresent(accountId, payload);
|
|
684
|
+
return payload;
|
|
685
|
+
} catch (error) {
|
|
686
|
+
if (isMissingSecretStoreEntryError(error)) markMissing(accountId);
|
|
687
|
+
throw error;
|
|
688
|
+
} finally {
|
|
689
|
+
inFlightLoads.delete(accountId);
|
|
690
|
+
}
|
|
691
|
+
})();
|
|
692
|
+
inFlightLoads.set(accountId, promise);
|
|
693
|
+
return promise;
|
|
694
|
+
};
|
|
695
|
+
return {
|
|
696
|
+
id: adapter.id,
|
|
697
|
+
label: adapter.label,
|
|
698
|
+
getServiceName: (accountId) => adapter.getServiceName(accountId),
|
|
699
|
+
save: async (accountId, payload) => {
|
|
700
|
+
await adapter.save(accountId, payload);
|
|
701
|
+
markPresent(accountId, payload);
|
|
702
|
+
},
|
|
703
|
+
load: async (accountId) => {
|
|
704
|
+
const cachedPayload = payloadCache.get(accountId);
|
|
705
|
+
if (cachedPayload) return cachedPayload;
|
|
706
|
+
if (missingAccounts.has(accountId)) throw createMissingSecretStoreEntryError(accountId);
|
|
707
|
+
return loadAndCache(accountId);
|
|
708
|
+
},
|
|
709
|
+
delete: async (accountId) => {
|
|
710
|
+
try {
|
|
711
|
+
await adapter.delete(accountId);
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (isMissingSecretStoreEntryError(error)) markMissing(accountId);
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
markMissing(accountId);
|
|
717
|
+
},
|
|
718
|
+
exists: async (accountId) => {
|
|
719
|
+
if (payloadCache.has(accountId)) return true;
|
|
720
|
+
if (missingAccounts.has(accountId)) return false;
|
|
721
|
+
const cachedExists = existsCache.get(accountId);
|
|
722
|
+
if (cachedExists !== void 0) return cachedExists;
|
|
723
|
+
try {
|
|
724
|
+
await loadAndCache(accountId);
|
|
725
|
+
return true;
|
|
726
|
+
} catch (error) {
|
|
727
|
+
if (isMissingSecretStoreEntryError(error)) return false;
|
|
728
|
+
}
|
|
729
|
+
const exists = await adapter.exists(accountId);
|
|
730
|
+
existsCache.set(accountId, exists);
|
|
731
|
+
if (!exists) missingAccounts.add(accountId);
|
|
732
|
+
return exists;
|
|
733
|
+
},
|
|
734
|
+
listAccountIds: async () => {
|
|
735
|
+
const accountIds = await adapter.listAccountIds();
|
|
736
|
+
for (const accountId of accountIds) {
|
|
737
|
+
existsCache.set(accountId, true);
|
|
738
|
+
missingAccounts.delete(accountId);
|
|
739
|
+
}
|
|
740
|
+
return accountIds;
|
|
741
|
+
},
|
|
742
|
+
getCapability: () => adapter.getCapability(),
|
|
743
|
+
[CACHED_ADAPTER_SYMBOL]: true
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
const unsupportedError = (platform) => /* @__PURE__ */ new Error(`No default secret store adapter configured for platform '${platform}'. Only macOS, Windows, and Linux adapters are wired by default right now.`);
|
|
747
|
+
const loadConfiguredAccountIds = async () => {
|
|
748
|
+
if (!configExists()) return [];
|
|
749
|
+
return (await loadConfig()).accounts.map((account) => account.accountId);
|
|
750
|
+
};
|
|
751
|
+
const createMacOSCrossKeychainAdapter = () => ({
|
|
752
|
+
id: "macos-cross-keychain",
|
|
753
|
+
label: "macOS Keychain (cross-keychain)",
|
|
754
|
+
getServiceName: getMacOSCrossKeychainService,
|
|
755
|
+
save: saveMacOSCrossKeychainPayload,
|
|
756
|
+
load: loadMacOSCrossKeychainPayload,
|
|
757
|
+
delete: deleteMacOSCrossKeychainPayload,
|
|
758
|
+
exists: macosCrossKeychainPayloadExists,
|
|
759
|
+
listAccountIds: async () => {
|
|
760
|
+
const accountIds = await loadConfiguredAccountIds();
|
|
761
|
+
return (await Promise.all(accountIds.map(async (accountId) => ({
|
|
762
|
+
accountId,
|
|
763
|
+
exists: await macosCrossKeychainPayloadExists(accountId)
|
|
764
|
+
})))).filter((item) => item.exists).map((item) => item.accountId);
|
|
765
|
+
},
|
|
766
|
+
getCapability: () => ({ available: true })
|
|
767
|
+
});
|
|
768
|
+
const createMacOSLegacyKeychainAdapter = () => ({
|
|
769
|
+
id: "macos-legacy-keychain",
|
|
770
|
+
label: "macOS Keychain (legacy security CLI)",
|
|
771
|
+
getServiceName: getKeychainService,
|
|
772
|
+
save: async (accountId, payload) => {
|
|
773
|
+
saveKeychainPayload(accountId, payload);
|
|
774
|
+
},
|
|
775
|
+
load: async (accountId) => loadKeychainPayload(accountId),
|
|
776
|
+
delete: async (accountId) => {
|
|
777
|
+
deleteKeychainPayload(accountId);
|
|
778
|
+
},
|
|
779
|
+
exists: async (accountId) => keychainPayloadExists(accountId),
|
|
780
|
+
listAccountIds: async () => listKeychainAccounts(),
|
|
781
|
+
getCapability: () => ({ available: true })
|
|
782
|
+
});
|
|
783
|
+
const createWindowsCrossKeychainAdapter = () => ({
|
|
784
|
+
id: "windows-cross-keychain",
|
|
785
|
+
label: "Windows Credential Manager (cross-keychain)",
|
|
786
|
+
getServiceName: getWindowsCrossKeychainService,
|
|
787
|
+
save: saveWindowsCrossKeychainPayload,
|
|
788
|
+
load: loadWindowsCrossKeychainPayload,
|
|
789
|
+
delete: deleteWindowsCrossKeychainPayload,
|
|
790
|
+
exists: windowsCrossKeychainPayloadExists,
|
|
791
|
+
listAccountIds: async () => {
|
|
792
|
+
const accountIds = await loadConfiguredAccountIds();
|
|
793
|
+
return (await Promise.all(accountIds.map(async (accountId) => ({
|
|
794
|
+
accountId,
|
|
795
|
+
exists: await windowsCrossKeychainPayloadExists(accountId)
|
|
796
|
+
})))).filter((item) => item.exists).map((item) => item.accountId);
|
|
797
|
+
},
|
|
798
|
+
getCapability: () => ({ available: true })
|
|
799
|
+
});
|
|
800
|
+
const createLinuxCrossKeychainAdapter = () => ({
|
|
801
|
+
id: "linux-cross-keychain",
|
|
802
|
+
label: "Linux Secret Service (cross-keychain)",
|
|
803
|
+
getServiceName: getLinuxCrossKeychainService,
|
|
804
|
+
save: saveLinuxCrossKeychainPayload,
|
|
805
|
+
load: loadLinuxCrossKeychainPayload,
|
|
806
|
+
delete: deleteLinuxCrossKeychainPayload,
|
|
807
|
+
exists: linuxCrossKeychainPayloadExists,
|
|
808
|
+
listAccountIds: async () => {
|
|
809
|
+
const accountIds = await loadConfiguredAccountIds();
|
|
810
|
+
return (await Promise.all(accountIds.map(async (accountId) => ({
|
|
811
|
+
accountId,
|
|
812
|
+
exists: await linuxCrossKeychainPayloadExists(accountId)
|
|
813
|
+
})))).filter((item) => item.exists).map((item) => item.accountId);
|
|
814
|
+
},
|
|
815
|
+
getCapability: () => ({ available: true })
|
|
816
|
+
});
|
|
817
|
+
const createUnsupportedAdapter = (platform) => ({
|
|
818
|
+
id: "unsupported",
|
|
819
|
+
label: "Unsupported (no adapter configured)",
|
|
820
|
+
getServiceName: (accountId) => `cdx-openai-${accountId}`,
|
|
821
|
+
save: async () => {
|
|
822
|
+
throw unsupportedError(platform);
|
|
823
|
+
},
|
|
824
|
+
load: async () => {
|
|
825
|
+
throw unsupportedError(platform);
|
|
826
|
+
},
|
|
827
|
+
delete: async () => {
|
|
828
|
+
throw unsupportedError(platform);
|
|
829
|
+
},
|
|
830
|
+
exists: async () => false,
|
|
831
|
+
listAccountIds: async () => [],
|
|
832
|
+
getCapability: () => ({
|
|
833
|
+
available: false,
|
|
834
|
+
reason: "No default secure-store adapter available for this platform."
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
const createRuntimeSecretStoreAdapter = (platform = process.platform) => {
|
|
838
|
+
if (platform === "darwin") return createMacOSCrossKeychainAdapter();
|
|
839
|
+
if (platform === "win32") return createWindowsCrossKeychainAdapter();
|
|
840
|
+
if (platform === "linux") return createLinuxCrossKeychainAdapter();
|
|
841
|
+
return createUnsupportedAdapter(platform);
|
|
842
|
+
};
|
|
843
|
+
const createSecretStoreAdapterFromSelection = (selection = "auto", platform = process.platform) => {
|
|
844
|
+
if (selection === "legacy-keychain") {
|
|
845
|
+
if (platform !== "darwin") throw new Error("The legacy keychain adapter is only available on macOS (darwin).");
|
|
846
|
+
return createMacOSLegacyKeychainAdapter();
|
|
847
|
+
}
|
|
848
|
+
return createRuntimeSecretStoreAdapter(platform);
|
|
849
|
+
};
|
|
850
|
+
const resolveMacOSCrossKeychainBackendId = async (platform = process.platform) => {
|
|
851
|
+
if (platform !== "darwin") return null;
|
|
852
|
+
return resolveMacOSCrossKeychainBackendId$1();
|
|
853
|
+
};
|
|
854
|
+
let currentSecretStoreAdapter = withSecretStoreCache(createRuntimeSecretStoreAdapter());
|
|
855
|
+
const getSecretStoreAdapter = () => currentSecretStoreAdapter;
|
|
856
|
+
const setSecretStoreAdapter = (adapter) => {
|
|
857
|
+
currentSecretStoreAdapter = withSecretStoreCache(adapter);
|
|
858
|
+
};
|
|
859
|
+
const resetSecretStoreAdapter = () => {
|
|
860
|
+
currentSecretStoreAdapter = withSecretStoreCache(createRuntimeSecretStoreAdapter());
|
|
861
|
+
};
|
|
862
|
+
const getSecretStoreCapability = () => {
|
|
863
|
+
const adapter = getSecretStoreAdapter();
|
|
864
|
+
const capability = adapter.getCapability();
|
|
865
|
+
return {
|
|
866
|
+
id: adapter.id,
|
|
867
|
+
label: adapter.label,
|
|
868
|
+
available: capability.available,
|
|
869
|
+
...capability.reason ? { reason: capability.reason } : {}
|
|
870
|
+
};
|
|
871
|
+
};
|
|
872
|
+
|
|
242
873
|
//#endregion
|
|
243
874
|
//#region lib/oauth/constants.ts
|
|
244
875
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
@@ -437,31 +1068,27 @@ const startOAuthServer = (state) => {
|
|
|
437
1068
|
//#endregion
|
|
438
1069
|
//#region lib/oauth/login.ts
|
|
439
1070
|
const openBrowser = (url) => {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
stdio: "ignore"
|
|
445
|
-
}).unref();
|
|
446
|
-
} catch (error) {
|
|
447
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
448
|
-
p.log.warning(`Could not auto-open browser (${msg}).`);
|
|
1071
|
+
const result = openBrowserUrl(url);
|
|
1072
|
+
if (!result.ok) {
|
|
1073
|
+
const msg = result.error ?? "unknown error";
|
|
1074
|
+
p.log.warning(`Could not auto-open browser via ${result.launcher.label} (${msg}).`);
|
|
449
1075
|
}
|
|
450
1076
|
};
|
|
451
1077
|
const addAccountToConfig = async (accountId, label) => {
|
|
452
1078
|
let config;
|
|
1079
|
+
const secretStore = getSecretStoreAdapter();
|
|
453
1080
|
if (configExists()) {
|
|
454
1081
|
config = await loadConfig();
|
|
455
1082
|
if (!config.accounts.some((a) => a.accountId === accountId)) config.accounts.push({
|
|
456
1083
|
accountId,
|
|
457
|
-
keychainService:
|
|
1084
|
+
keychainService: secretStore.getServiceName(accountId),
|
|
458
1085
|
...label ? { label } : {}
|
|
459
1086
|
});
|
|
460
1087
|
} else config = {
|
|
461
1088
|
current: 0,
|
|
462
1089
|
accounts: [{
|
|
463
1090
|
accountId,
|
|
464
|
-
keychainService:
|
|
1091
|
+
keychainService: secretStore.getServiceName(accountId),
|
|
465
1092
|
...label ? { label } : {}
|
|
466
1093
|
}]
|
|
467
1094
|
};
|
|
@@ -521,16 +1148,17 @@ const performRefresh = async (targetAccountId, label, options = {}) => {
|
|
|
521
1148
|
}
|
|
522
1149
|
if (spinner) spinner.message("Updating credentials...");
|
|
523
1150
|
else p.log.message("Updating credentials...");
|
|
524
|
-
|
|
1151
|
+
const payload = {
|
|
525
1152
|
refresh: tokenResult.refresh,
|
|
526
1153
|
access: tokenResult.access,
|
|
527
1154
|
expires: tokenResult.expires,
|
|
528
1155
|
accountId: newAccountId,
|
|
529
1156
|
...tokenResult.idToken ? { idToken: tokenResult.idToken } : {}
|
|
530
|
-
}
|
|
1157
|
+
};
|
|
1158
|
+
await getSecretStoreAdapter().save(newAccountId, payload);
|
|
531
1159
|
if (spinner) spinner.stop("Credentials refreshed!");
|
|
532
1160
|
else p.log.success("Credentials refreshed!");
|
|
533
|
-
p.log.success(`Account "${displayName}" credentials updated in
|
|
1161
|
+
p.log.success(`Account "${displayName}" credentials updated in secure store.`);
|
|
534
1162
|
return { accountId: newAccountId };
|
|
535
1163
|
} finally {
|
|
536
1164
|
clearInterval(keepAlive);
|
|
@@ -568,13 +1196,14 @@ const performLogin = async () => {
|
|
|
568
1196
|
return null;
|
|
569
1197
|
}
|
|
570
1198
|
spinner.message("Saving credentials...");
|
|
571
|
-
|
|
1199
|
+
const payload = {
|
|
572
1200
|
refresh: tokenResult.refresh,
|
|
573
1201
|
access: tokenResult.access,
|
|
574
1202
|
expires: tokenResult.expires,
|
|
575
1203
|
accountId,
|
|
576
1204
|
...tokenResult.idToken ? { idToken: tokenResult.idToken } : {}
|
|
577
|
-
}
|
|
1205
|
+
};
|
|
1206
|
+
await getSecretStoreAdapter().save(accountId, payload);
|
|
578
1207
|
spinner.stop("Login successful!");
|
|
579
1208
|
const labelInput = await p.text({
|
|
580
1209
|
message: "Enter a label for this account (or press Enter to skip):",
|
|
@@ -583,7 +1212,7 @@ const performLogin = async () => {
|
|
|
583
1212
|
const label = !p.isCancel(labelInput) && labelInput?.trim() ? labelInput.trim() : void 0;
|
|
584
1213
|
await addAccountToConfig(accountId, label);
|
|
585
1214
|
const displayName = label ?? accountId;
|
|
586
|
-
p.log.success(`Account "${displayName}" saved to
|
|
1215
|
+
p.log.success(`Account "${displayName}" saved to secure store and config.`);
|
|
587
1216
|
p.outro("You can now use 'cdx switch' to activate this account.");
|
|
588
1217
|
return { accountId };
|
|
589
1218
|
};
|
|
@@ -595,7 +1224,19 @@ const writeActiveAuthFilesIfCurrent = async (accountId) => {
|
|
|
595
1224
|
const config = await loadConfig();
|
|
596
1225
|
const current = config.accounts[config.current];
|
|
597
1226
|
if (!current || current.accountId !== accountId) return null;
|
|
598
|
-
return writeAllAuthFiles(
|
|
1227
|
+
return writeAllAuthFiles(await getSecretStoreAdapter().load(accountId));
|
|
1228
|
+
};
|
|
1229
|
+
|
|
1230
|
+
//#endregion
|
|
1231
|
+
//#region lib/platform/capabilities.ts
|
|
1232
|
+
const getRuntimeCapabilities = () => {
|
|
1233
|
+
const pathResolution = getPathResolutionInfo();
|
|
1234
|
+
return {
|
|
1235
|
+
platform: process.platform,
|
|
1236
|
+
pathProfile: pathResolution.profile,
|
|
1237
|
+
secretStore: getSecretStoreCapability(),
|
|
1238
|
+
browserLauncher: getBrowserLauncherCapability(process.platform)
|
|
1239
|
+
};
|
|
599
1240
|
};
|
|
600
1241
|
|
|
601
1242
|
//#endregion
|
|
@@ -680,20 +1321,24 @@ const readPiAuthAccount = async () => {
|
|
|
680
1321
|
};
|
|
681
1322
|
}
|
|
682
1323
|
};
|
|
683
|
-
const getAccountStatus = (accountId, isCurrent, label) => {
|
|
684
|
-
const
|
|
1324
|
+
const getAccountStatus = async (accountId, isCurrent, label) => {
|
|
1325
|
+
const secretStore = getSecretStoreAdapter();
|
|
1326
|
+
let secureStoreExists = false;
|
|
685
1327
|
let expiresAt = null;
|
|
686
1328
|
let hasIdToken = false;
|
|
687
|
-
|
|
688
|
-
const payload =
|
|
1329
|
+
try {
|
|
1330
|
+
const payload = await secretStore.load(accountId);
|
|
1331
|
+
secureStoreExists = true;
|
|
689
1332
|
expiresAt = payload.expires;
|
|
690
1333
|
hasIdToken = !!payload.idToken;
|
|
691
|
-
} catch {
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
secureStoreExists = !isMissingSecretStoreEntryError(error);
|
|
1336
|
+
}
|
|
692
1337
|
return {
|
|
693
1338
|
accountId,
|
|
694
1339
|
label,
|
|
695
1340
|
isCurrent,
|
|
696
|
-
|
|
1341
|
+
secureStoreExists,
|
|
697
1342
|
hasIdToken,
|
|
698
1343
|
expiresAt,
|
|
699
1344
|
expiresIn: formatExpiry(expiresAt)
|
|
@@ -705,7 +1350,7 @@ const getStatus = async () => {
|
|
|
705
1350
|
const config = await loadConfig();
|
|
706
1351
|
for (let i = 0; i < config.accounts.length; i++) {
|
|
707
1352
|
const account = config.accounts[i];
|
|
708
|
-
accounts.push(getAccountStatus(account.accountId, i === config.current, account.label));
|
|
1353
|
+
accounts.push(await getAccountStatus(account.accountId, i === config.current, account.label));
|
|
709
1354
|
}
|
|
710
1355
|
}
|
|
711
1356
|
const [opencodeAuth, codexAuth, piAuth] = await Promise.all([
|
|
@@ -717,7 +1362,8 @@ const getStatus = async () => {
|
|
|
717
1362
|
accounts,
|
|
718
1363
|
opencodeAuth,
|
|
719
1364
|
codexAuth,
|
|
720
|
-
piAuth
|
|
1365
|
+
piAuth,
|
|
1366
|
+
capabilities: getRuntimeCapabilities()
|
|
721
1367
|
};
|
|
722
1368
|
};
|
|
723
1369
|
|
|
@@ -727,10 +1373,16 @@ const getAccountDisplay = (accountId, isCurrent, label) => {
|
|
|
727
1373
|
const name = label ? `${label} (${accountId})` : accountId;
|
|
728
1374
|
return isCurrent ? `${name} (current)` : name;
|
|
729
1375
|
};
|
|
730
|
-
const
|
|
731
|
-
|
|
1376
|
+
const hasStoredCredentials = async (accountId) => getSecretStoreAdapter().exists(accountId);
|
|
1377
|
+
const loadStoredCredentials = async (accountId) => getSecretStoreAdapter().load(accountId);
|
|
1378
|
+
const getStoredAccountIds = async () => getSecretStoreAdapter().listAccountIds();
|
|
1379
|
+
const removeStoredCredentials = async (accountId) => {
|
|
1380
|
+
await getSecretStoreAdapter().delete(accountId);
|
|
1381
|
+
};
|
|
1382
|
+
const getRefreshExpiryState = async (accountId) => {
|
|
1383
|
+
if (!await hasStoredCredentials(accountId)) return "unknown [no secure store entry]";
|
|
732
1384
|
try {
|
|
733
|
-
return formatExpiry(
|
|
1385
|
+
return formatExpiry((await loadStoredCredentials(accountId)).expires);
|
|
734
1386
|
} catch {
|
|
735
1387
|
return "unknown";
|
|
736
1388
|
}
|
|
@@ -746,7 +1398,7 @@ const handleListAccounts = async () => {
|
|
|
746
1398
|
for (const account of config.accounts) {
|
|
747
1399
|
const marker = account.accountId === currentAccountId ? "→ " : " ";
|
|
748
1400
|
const displayName = account.label ? `${account.label} (${account.accountId})` : account.accountId;
|
|
749
|
-
const status =
|
|
1401
|
+
const status = await hasStoredCredentials(account.accountId) ? "" : " (missing credentials)";
|
|
750
1402
|
p.log.message(`${marker}${displayName}${status}`);
|
|
751
1403
|
}
|
|
752
1404
|
};
|
|
@@ -784,7 +1436,7 @@ const handleSwitchAccount = async () => {
|
|
|
784
1436
|
}
|
|
785
1437
|
let payload;
|
|
786
1438
|
try {
|
|
787
|
-
payload =
|
|
1439
|
+
payload = await loadStoredCredentials(selectedAccount.accountId);
|
|
788
1440
|
} catch {
|
|
789
1441
|
p.log.error(`Missing credentials for account ${selectedAccount.label ?? selectedAccount.accountId}. Re-login with 'cdx login'.`);
|
|
790
1442
|
return;
|
|
@@ -815,10 +1467,10 @@ const handleReloginAccount = async () => {
|
|
|
815
1467
|
return;
|
|
816
1468
|
}
|
|
817
1469
|
const currentAccountId = config.accounts[config.current]?.accountId;
|
|
818
|
-
const options = config.accounts.map((account) => ({
|
|
1470
|
+
const options = await Promise.all(config.accounts.map(async (account) => ({
|
|
819
1471
|
value: account.accountId,
|
|
820
|
-
label: `${getAccountDisplay(account.accountId, account.accountId === currentAccountId, account.label)} — ${getRefreshExpiryState(account.accountId)}`
|
|
821
|
-
}));
|
|
1472
|
+
label: `${getAccountDisplay(account.accountId, account.accountId === currentAccountId, account.label)} — ${await getRefreshExpiryState(account.accountId)}`
|
|
1473
|
+
})));
|
|
822
1474
|
const selected = await p.select({
|
|
823
1475
|
message: "Select account to re-login:",
|
|
824
1476
|
options
|
|
@@ -829,7 +1481,7 @@ const handleReloginAccount = async () => {
|
|
|
829
1481
|
}
|
|
830
1482
|
const accountId = selected;
|
|
831
1483
|
const account = config.accounts.find((a) => a.accountId === accountId);
|
|
832
|
-
const expiryState = getRefreshExpiryState(accountId);
|
|
1484
|
+
const expiryState = await getRefreshExpiryState(accountId);
|
|
833
1485
|
const displayName = account?.label ?? accountId;
|
|
834
1486
|
p.log.info(`Current token status for ${displayName}: ${expiryState}`);
|
|
835
1487
|
try {
|
|
@@ -884,7 +1536,7 @@ const handleRemoveAccount = async () => {
|
|
|
884
1536
|
return;
|
|
885
1537
|
}
|
|
886
1538
|
try {
|
|
887
|
-
|
|
1539
|
+
await removeStoredCredentials(accountId);
|
|
888
1540
|
} catch {}
|
|
889
1541
|
const previousAccountId = config.accounts[config.current]?.accountId;
|
|
890
1542
|
config.accounts = config.accounts.filter((a) => a.accountId !== accountId);
|
|
@@ -948,9 +1600,9 @@ const handleStatus = async () => {
|
|
|
948
1600
|
for (const account of status.accounts) {
|
|
949
1601
|
const marker = account.isCurrent ? "→ " : " ";
|
|
950
1602
|
const name = account.label ? `${account.label} (${account.accountId})` : account.accountId;
|
|
951
|
-
const
|
|
1603
|
+
const secureStore = account.secureStoreExists ? "" : " [no secure store entry]";
|
|
952
1604
|
const idToken = account.hasIdToken ? "" : " [no id_token]";
|
|
953
|
-
p.log.message(`${marker}${name} — ${account.expiresIn}${
|
|
1605
|
+
p.log.message(`${marker}${name} — ${account.expiresIn}${secureStore}${idToken}`);
|
|
954
1606
|
}
|
|
955
1607
|
const ocStatus = status.opencodeAuth.exists ? `active: ${status.opencodeAuth.accountId ?? "unknown"}` : "not found";
|
|
956
1608
|
const cxStatus = status.codexAuth.exists ? `active: ${status.codexAuth.accountId ?? "unknown"}` : "not found";
|
|
@@ -964,7 +1616,7 @@ const runInteractiveMode = async () => {
|
|
|
964
1616
|
p.intro("cdx - OpenAI Account Switcher");
|
|
965
1617
|
let running = true;
|
|
966
1618
|
while (running) {
|
|
967
|
-
const
|
|
1619
|
+
const storedAccounts = await getStoredAccountIds();
|
|
968
1620
|
let currentInfo = "";
|
|
969
1621
|
if (configExists()) try {
|
|
970
1622
|
const config = await loadConfig();
|
|
@@ -976,7 +1628,7 @@ const runInteractiveMode = async () => {
|
|
|
976
1628
|
options: [
|
|
977
1629
|
{
|
|
978
1630
|
value: "list",
|
|
979
|
-
label: `List accounts (${
|
|
1631
|
+
label: `List accounts (${storedAccounts.length} in secure store)`
|
|
980
1632
|
},
|
|
981
1633
|
{
|
|
982
1634
|
value: "switch",
|
|
@@ -1055,6 +1707,164 @@ const registerDefaultInteractiveAction = (program) => {
|
|
|
1055
1707
|
});
|
|
1056
1708
|
};
|
|
1057
1709
|
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region lib/keychain-acl.ts
|
|
1712
|
+
const getDefaultMap = (services) => {
|
|
1713
|
+
const map = /* @__PURE__ */ new Map();
|
|
1714
|
+
for (const service of services) map.set(service, {
|
|
1715
|
+
service,
|
|
1716
|
+
mode: "missing",
|
|
1717
|
+
applications: []
|
|
1718
|
+
});
|
|
1719
|
+
return map;
|
|
1720
|
+
};
|
|
1721
|
+
const parseItemEntries = (block) => {
|
|
1722
|
+
const entries = [];
|
|
1723
|
+
const entryRegex = /entry\s+\d+:\n([\s\S]*?)(?=\n\s*entry\s+\d+:|$)/g;
|
|
1724
|
+
let match;
|
|
1725
|
+
while ((match = entryRegex.exec(block)) !== null) if (match[1]) entries.push(match[1]);
|
|
1726
|
+
return entries;
|
|
1727
|
+
};
|
|
1728
|
+
const parseApplicationsFromEntry = (entry) => {
|
|
1729
|
+
const applications = [];
|
|
1730
|
+
const appRegex = /^\s*\d+:\s+(.+?)(?:\s+\([^\n]*\))?\s*$/gm;
|
|
1731
|
+
let match;
|
|
1732
|
+
while ((match = appRegex.exec(entry)) !== null) {
|
|
1733
|
+
const app = match[1]?.trim();
|
|
1734
|
+
if (app) applications.push(app);
|
|
1735
|
+
}
|
|
1736
|
+
return applications;
|
|
1737
|
+
};
|
|
1738
|
+
const parseKeychainDecryptAccessFromDump = (dumpOutput, services) => {
|
|
1739
|
+
const dedupedServices = [...new Set(services.filter((service) => service.length > 0))];
|
|
1740
|
+
const result = getDefaultMap(dedupedServices);
|
|
1741
|
+
if (dedupedServices.length === 0 || !dumpOutput.trim()) return result;
|
|
1742
|
+
const targetServices = new Set(dedupedServices);
|
|
1743
|
+
const blocks = dumpOutput.split(/\n(?=keychain:\s+")/g);
|
|
1744
|
+
for (const block of blocks) {
|
|
1745
|
+
if (!block.startsWith("keychain:")) continue;
|
|
1746
|
+
const service = block.match(/"svce"<blob>="([^"]+)"/)?.[1];
|
|
1747
|
+
if (!service || !targetServices.has(service)) continue;
|
|
1748
|
+
const entries = parseItemEntries(block);
|
|
1749
|
+
let mode = "missing";
|
|
1750
|
+
const applications = [];
|
|
1751
|
+
for (const entry of entries) {
|
|
1752
|
+
const authorizationsLine = entry.match(/authorizations\s*\(\d+\):\s*([^\n]+)/)?.[1] ?? "";
|
|
1753
|
+
if (!/\bdecrypt\b/.test(authorizationsLine)) continue;
|
|
1754
|
+
if (/applications:\s*<null>/.test(entry)) {
|
|
1755
|
+
mode = "all-apps";
|
|
1756
|
+
applications.length = 0;
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
const entryApplications = parseApplicationsFromEntry(entry);
|
|
1760
|
+
if (entryApplications.length > 0) {
|
|
1761
|
+
mode = "explicit-list";
|
|
1762
|
+
for (const app of entryApplications) if (!applications.includes(app)) applications.push(app);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
result.set(service, {
|
|
1766
|
+
service,
|
|
1767
|
+
mode,
|
|
1768
|
+
applications
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
return result;
|
|
1772
|
+
};
|
|
1773
|
+
const getKeychainDecryptAccessByServiceAsync = async (services) => {
|
|
1774
|
+
const dedupedServices = [...new Set(services.filter((service) => service.length > 0))];
|
|
1775
|
+
const defaultResult = getDefaultMap(dedupedServices);
|
|
1776
|
+
if (process.platform !== "darwin" || dedupedServices.length === 0) return defaultResult;
|
|
1777
|
+
const dumpResult = await runSecuritySafeAsync(["dump-keychain", "-a"]);
|
|
1778
|
+
if (!dumpResult.success) return defaultResult;
|
|
1779
|
+
return parseKeychainDecryptAccessFromDump(dumpResult.output, dedupedServices);
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
//#endregion
|
|
1783
|
+
//#region lib/commands/doctor.ts
|
|
1784
|
+
const hasRuntimeTrustedApp = (trustedApplications, runtimeExecutablePath) => {
|
|
1785
|
+
const runtimeBaseName = path.basename(runtimeExecutablePath).toLowerCase();
|
|
1786
|
+
return trustedApplications.some((trustedApp) => {
|
|
1787
|
+
if (trustedApp === runtimeExecutablePath) return true;
|
|
1788
|
+
return path.basename(trustedApp).toLowerCase() === runtimeBaseName;
|
|
1789
|
+
});
|
|
1790
|
+
};
|
|
1791
|
+
const registerDoctorCommand = (program) => {
|
|
1792
|
+
program.command("doctor").description("Show auth file paths and runtime capabilities").option("--check-keychain-acl", "Run keychain trusted-app/ACL checks on macOS (can be slow)").action(async (options) => {
|
|
1793
|
+
try {
|
|
1794
|
+
const status = await getStatus();
|
|
1795
|
+
const paths = getPaths();
|
|
1796
|
+
const resolveLabel = (accountId) => {
|
|
1797
|
+
if (!accountId) return "unknown";
|
|
1798
|
+
return status.accounts.find((account) => account.accountId === accountId)?.label ?? accountId;
|
|
1799
|
+
};
|
|
1800
|
+
process.stdout.write("\nAuth files:\n");
|
|
1801
|
+
const ocStatus = status.opencodeAuth.exists ? `active: ${resolveLabel(status.opencodeAuth.accountId)}` : "not found";
|
|
1802
|
+
process.stdout.write(` OpenCode: ${ocStatus}\n`);
|
|
1803
|
+
process.stdout.write(` Path: ${paths.authPath}\n`);
|
|
1804
|
+
const cxStatus = status.codexAuth.exists ? `active: ${resolveLabel(status.codexAuth.accountId)}` : "not found";
|
|
1805
|
+
process.stdout.write(` Codex CLI: ${cxStatus}\n`);
|
|
1806
|
+
process.stdout.write(` Path: ${paths.codexAuthPath}\n`);
|
|
1807
|
+
const piStatus = status.piAuth.exists ? `active: ${resolveLabel(status.piAuth.accountId)}` : "not found";
|
|
1808
|
+
process.stdout.write(` Pi Agent: ${piStatus}\n`);
|
|
1809
|
+
process.stdout.write(` Path: ${paths.piAuthPath}\n`);
|
|
1810
|
+
process.stdout.write("\nCapabilities:\n");
|
|
1811
|
+
process.stdout.write(` Platform: ${status.capabilities.platform}\n`);
|
|
1812
|
+
process.stdout.write(` Path profile: ${status.capabilities.pathProfile}\n`);
|
|
1813
|
+
const secretStoreState = status.capabilities.secretStore.available ? "available" : `unavailable${status.capabilities.secretStore.reason ? ` (${status.capabilities.secretStore.reason})` : ""}`;
|
|
1814
|
+
process.stdout.write(` Secret store: ${status.capabilities.secretStore.label} — ${secretStoreState}\n`);
|
|
1815
|
+
const browserState = status.capabilities.browserLauncher.available ? "available" : "not found";
|
|
1816
|
+
process.stdout.write(` Browser launcher: ${status.capabilities.browserLauncher.label} — ${browserState}\n`);
|
|
1817
|
+
if (process.platform === "darwin" && !options.checkKeychainAcl) {
|
|
1818
|
+
process.stdout.write(" ┌─ Optional keychain ACL check\n");
|
|
1819
|
+
process.stdout.write(" │ Run: cdx doctor --check-keychain-acl\n");
|
|
1820
|
+
process.stdout.write(" │ Verifies whether your current runtime is trusted by Keychain.\n");
|
|
1821
|
+
process.stdout.write(" └─ Expected duration: ~30-60 seconds\n");
|
|
1822
|
+
}
|
|
1823
|
+
if (process.platform === "darwin" && options.checkKeychainAcl) {
|
|
1824
|
+
const secretStore = getSecretStoreAdapter();
|
|
1825
|
+
const accountsWithSecrets = status.accounts.filter((account) => account.secureStoreExists);
|
|
1826
|
+
if (accountsWithSecrets.length > 0) {
|
|
1827
|
+
const runtimeExecutablePath = process.execPath;
|
|
1828
|
+
const services = accountsWithSecrets.map((account) => secretStore.getServiceName(account.accountId));
|
|
1829
|
+
process.stdout.write("\nKeychain ACL checks:\n");
|
|
1830
|
+
process.stdout.write(` Runtime executable: ${runtimeExecutablePath}\n`);
|
|
1831
|
+
const aclSpinner = p.spinner();
|
|
1832
|
+
const accountWord = accountsWithSecrets.length === 1 ? "account" : "accounts";
|
|
1833
|
+
aclSpinner.start(`Checking keychain ACLs for ${accountsWithSecrets.length} ${accountWord}...`);
|
|
1834
|
+
const decryptAccessByService = await getKeychainDecryptAccessByServiceAsync(services);
|
|
1835
|
+
aclSpinner.stop("Keychain ACL checks complete.");
|
|
1836
|
+
for (const account of accountsWithSecrets) {
|
|
1837
|
+
const service = secretStore.getServiceName(account.accountId);
|
|
1838
|
+
const decryptAccess = decryptAccessByService.get(service);
|
|
1839
|
+
const accountLabel = resolveLabel(account.accountId);
|
|
1840
|
+
if (!decryptAccess || decryptAccess.mode === "missing") {
|
|
1841
|
+
process.stdout.write(` ${accountLabel}: unable to read decrypt trusted apps (service: ${service})\n`);
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
if (decryptAccess.mode === "all-apps") {
|
|
1845
|
+
process.stdout.write(` ${accountLabel}: decrypt access allows all apps (<null>)\n`);
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
const runtimeTrusted = hasRuntimeTrustedApp(decryptAccess.applications, runtimeExecutablePath);
|
|
1849
|
+
const trustedAppsList = decryptAccess.applications.join(", ");
|
|
1850
|
+
if (runtimeTrusted) {
|
|
1851
|
+
process.stdout.write(` ${accountLabel}: runtime is in trusted apps\n`);
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
process.stdout.write(` ⚠ ${accountLabel}: runtime not found in trusted apps\n`);
|
|
1855
|
+
process.stdout.write(` Service: ${service}\n`);
|
|
1856
|
+
process.stdout.write(` Trusted apps: ${trustedAppsList || "(none)"}\n`);
|
|
1857
|
+
process.stdout.write(" This secret may have been created with a different runtime/toolchain (for example node vs bun).\n");
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
process.stdout.write("\n");
|
|
1862
|
+
} catch (error) {
|
|
1863
|
+
exitWithCommandError(error);
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
};
|
|
1867
|
+
|
|
1058
1868
|
//#endregion
|
|
1059
1869
|
//#region lib/commands/help.ts
|
|
1060
1870
|
const registerHelpCommand = (program) => {
|
|
@@ -1110,6 +1920,119 @@ const registerLoginCommand = (program, deps = {}) => {
|
|
|
1110
1920
|
});
|
|
1111
1921
|
};
|
|
1112
1922
|
|
|
1923
|
+
//#endregion
|
|
1924
|
+
//#region lib/secrets/migrate.ts
|
|
1925
|
+
const asErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
1926
|
+
const migrateLegacyMacOSSecrets = async (options = {}) => {
|
|
1927
|
+
if ((options.platform ?? process.platform) !== "darwin") throw new Error("'migrate-secrets' is only available on macOS (darwin).");
|
|
1928
|
+
const loadConfigFn = options.loadConfigFn ?? loadConfig;
|
|
1929
|
+
const saveConfigFn = options.saveConfigFn ?? saveConfig;
|
|
1930
|
+
const sourceAdapter = options.sourceAdapter ?? createMacOSLegacyKeychainAdapter();
|
|
1931
|
+
const targetAdapter = options.targetAdapter ?? createRuntimeSecretStoreAdapter("darwin");
|
|
1932
|
+
const config = await loadConfigFn();
|
|
1933
|
+
const accountResults = [];
|
|
1934
|
+
for (const account of config.accounts) {
|
|
1935
|
+
const accountId = account.accountId;
|
|
1936
|
+
try {
|
|
1937
|
+
if (!await sourceAdapter.exists(accountId)) {
|
|
1938
|
+
accountResults.push({
|
|
1939
|
+
accountId,
|
|
1940
|
+
label: account.label,
|
|
1941
|
+
status: "skipped",
|
|
1942
|
+
message: "No legacy keychain entry found."
|
|
1943
|
+
});
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
const loaded = await sourceAdapter.load(accountId);
|
|
1947
|
+
const payload = loaded.accountId === accountId ? loaded : {
|
|
1948
|
+
...loaded,
|
|
1949
|
+
accountId
|
|
1950
|
+
};
|
|
1951
|
+
await sourceAdapter.delete(accountId);
|
|
1952
|
+
try {
|
|
1953
|
+
await targetAdapter.save(accountId, payload);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
try {
|
|
1956
|
+
await sourceAdapter.save(accountId, payload);
|
|
1957
|
+
} catch {}
|
|
1958
|
+
throw error;
|
|
1959
|
+
}
|
|
1960
|
+
accountResults.push({
|
|
1961
|
+
accountId,
|
|
1962
|
+
label: account.label,
|
|
1963
|
+
status: "migrated",
|
|
1964
|
+
message: "Legacy entry migrated to cross-keychain backend."
|
|
1965
|
+
});
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
accountResults.push({
|
|
1968
|
+
accountId,
|
|
1969
|
+
label: account.label,
|
|
1970
|
+
status: "failed",
|
|
1971
|
+
message: asErrorMessage(error)
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
const failed = accountResults.filter((entry) => entry.status === "failed").length;
|
|
1976
|
+
const skipped = accountResults.filter((entry) => entry.status === "skipped").length;
|
|
1977
|
+
const migrated = accountResults.filter((entry) => entry.status === "migrated").length;
|
|
1978
|
+
let configUpdated = false;
|
|
1979
|
+
if (failed === 0) {
|
|
1980
|
+
let changed = false;
|
|
1981
|
+
for (const account of config.accounts) {
|
|
1982
|
+
const expectedService = targetAdapter.getServiceName(account.accountId);
|
|
1983
|
+
if (account.keychainService !== expectedService) {
|
|
1984
|
+
account.keychainService = expectedService;
|
|
1985
|
+
changed = true;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
if (config.secretStore !== "auto") {
|
|
1989
|
+
config.secretStore = "auto";
|
|
1990
|
+
changed = true;
|
|
1991
|
+
}
|
|
1992
|
+
if (changed) {
|
|
1993
|
+
await saveConfigFn(config);
|
|
1994
|
+
configUpdated = true;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return {
|
|
1998
|
+
migrated,
|
|
1999
|
+
skipped,
|
|
2000
|
+
failed,
|
|
2001
|
+
configUpdated,
|
|
2002
|
+
accountResults
|
|
2003
|
+
};
|
|
2004
|
+
};
|
|
2005
|
+
|
|
2006
|
+
//#endregion
|
|
2007
|
+
//#region lib/commands/migrate-secrets.ts
|
|
2008
|
+
const statusPrefix = (result) => {
|
|
2009
|
+
if (result.status === "migrated") return "✓";
|
|
2010
|
+
if (result.status === "skipped") return "-";
|
|
2011
|
+
return "✗";
|
|
2012
|
+
};
|
|
2013
|
+
const formatName = (result) => result.label ? `${result.label} (${result.accountId})` : result.accountId;
|
|
2014
|
+
const registerMigrateSecretsCommand = (program) => {
|
|
2015
|
+
program.command("migrate-secrets").description("Migrate macOS legacy keychain entries to cross-keychain and update config").action(async () => {
|
|
2016
|
+
try {
|
|
2017
|
+
const result = await migrateLegacyMacOSSecrets();
|
|
2018
|
+
process.stdout.write("\nSecret migration results:\n");
|
|
2019
|
+
for (const accountResult of result.accountResults) process.stdout.write(` ${statusPrefix(accountResult)} ${formatName(accountResult)}: ${accountResult.message}\n`);
|
|
2020
|
+
process.stdout.write("\nSummary:\n");
|
|
2021
|
+
process.stdout.write(` Migrated: ${result.migrated}\n`);
|
|
2022
|
+
process.stdout.write(` Skipped: ${result.skipped}\n`);
|
|
2023
|
+
process.stdout.write(` Failed: ${result.failed}\n`);
|
|
2024
|
+
if (result.failed === 0) {
|
|
2025
|
+
process.stdout.write(result.configUpdated ? " Config: updated (secretStore=auto, service names normalized)\n\n" : " Config: already up to date\n\n");
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
process.stdout.write(" Config: not updated because at least one account failed\n\n");
|
|
2029
|
+
throw new Error(`Migration finished with ${result.failed} failed account(s). Resolve them and run 'cdx migrate-secrets' again.`);
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
exitWithCommandError(error);
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
};
|
|
2035
|
+
|
|
1113
2036
|
//#endregion
|
|
1114
2037
|
//#region lib/commands/output.ts
|
|
1115
2038
|
const formatCodexMark = (result) => {
|
|
@@ -1144,14 +2067,15 @@ const registerReloginCommand = (program) => {
|
|
|
1144
2067
|
if (!target) throw new Error(`Account "${account}" not found. Use 'cdx login' to add it.`);
|
|
1145
2068
|
const displayName = target.label ?? target.accountId;
|
|
1146
2069
|
let expiryState = "unknown";
|
|
1147
|
-
let
|
|
1148
|
-
|
|
1149
|
-
|
|
2070
|
+
let secureStoreState = "";
|
|
2071
|
+
const secretStore = getSecretStoreAdapter();
|
|
2072
|
+
if (await secretStore.exists(target.accountId)) try {
|
|
2073
|
+
expiryState = formatExpiry((await secretStore.load(target.accountId)).expires);
|
|
1150
2074
|
} catch {
|
|
1151
2075
|
expiryState = "unknown";
|
|
1152
2076
|
}
|
|
1153
|
-
else
|
|
1154
|
-
process.stdout.write(`Current token status for ${displayName}: ${expiryState}${
|
|
2077
|
+
else secureStoreState = " [no secure store entry]";
|
|
2078
|
+
process.stdout.write(`Current token status for ${displayName}: ${expiryState}${secureStoreState}\n`);
|
|
1155
2079
|
const result = await performRefresh(target.accountId, target.label);
|
|
1156
2080
|
if (!result) {
|
|
1157
2081
|
process.stderr.write("Re-login failed.\n");
|
|
@@ -1188,9 +2112,10 @@ const fetchUsageRaw = async (accessToken, accountId) => {
|
|
|
1188
2112
|
* Fetches usage for an account. On 401, refreshes the token and retries once.
|
|
1189
2113
|
*/
|
|
1190
2114
|
const fetchUsage = async (accountId) => {
|
|
2115
|
+
const secretStore = getSecretStoreAdapter();
|
|
1191
2116
|
let payload;
|
|
1192
2117
|
try {
|
|
1193
|
-
payload =
|
|
2118
|
+
payload = await secretStore.load(accountId);
|
|
1194
2119
|
} catch (err) {
|
|
1195
2120
|
return {
|
|
1196
2121
|
ok: false,
|
|
@@ -1218,7 +2143,7 @@ const fetchUsage = async (accountId) => {
|
|
|
1218
2143
|
expires: refreshResult.expires,
|
|
1219
2144
|
idToken: refreshResult.idToken ?? payload.idToken
|
|
1220
2145
|
};
|
|
1221
|
-
|
|
2146
|
+
await secretStore.save(accountId, updatedPayload);
|
|
1222
2147
|
response = await fetchUsageRaw(updatedPayload.access, updatedPayload.accountId);
|
|
1223
2148
|
if (!response.ok) return {
|
|
1224
2149
|
ok: false,
|
|
@@ -1327,7 +2252,7 @@ const formatUsageOverview = (entries) => {
|
|
|
1327
2252
|
//#endregion
|
|
1328
2253
|
//#region lib/commands/status.ts
|
|
1329
2254
|
const registerStatusCommand = (program) => {
|
|
1330
|
-
program.command("status").description("Show account status, token expiry,
|
|
2255
|
+
program.command("status").description("Show account status, token expiry, and usage").action(async () => {
|
|
1331
2256
|
try {
|
|
1332
2257
|
const status = await getStatus();
|
|
1333
2258
|
if (status.accounts.length === 0) {
|
|
@@ -1339,31 +2264,43 @@ const registerStatusCommand = (program) => {
|
|
|
1339
2264
|
const account = status.accounts[i];
|
|
1340
2265
|
const marker = account.isCurrent ? "→ " : " ";
|
|
1341
2266
|
const warnings = [];
|
|
1342
|
-
if (!account.
|
|
2267
|
+
if (!account.secureStoreExists) warnings.push("[no secure store entry]");
|
|
1343
2268
|
if (!account.hasIdToken) warnings.push("[no id_token]");
|
|
1344
2269
|
const warnStr = warnings.length > 0 ? ` ${warnings.join(" ")}` : "";
|
|
1345
2270
|
const displayName = account.label ?? account.accountId;
|
|
1346
2271
|
process.stdout.write(`${marker}${displayName}${warnStr}\n`);
|
|
1347
2272
|
if (account.label) process.stdout.write(` ${account.accountId}\n`);
|
|
1348
2273
|
process.stdout.write(` ${account.expiresIn}\n`);
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2274
|
+
if (i < status.accounts.length - 1) process.stdout.write("\n");
|
|
2275
|
+
}
|
|
2276
|
+
const usageSpinner = p.spinner();
|
|
2277
|
+
const accountWord = status.accounts.length === 1 ? "account" : "accounts";
|
|
2278
|
+
usageSpinner.start(`Fetching usage for ${status.accounts.length} ${accountWord}...`);
|
|
2279
|
+
const usageResults = await Promise.allSettled(status.accounts.map((account) => fetchUsage(account.accountId)));
|
|
2280
|
+
const failedUsageCount = usageResults.filter((result) => result.status === "rejected" || result.status === "fulfilled" && !result.value.ok).length;
|
|
2281
|
+
if (failedUsageCount === 0) usageSpinner.stop("Usage loaded.");
|
|
2282
|
+
else {
|
|
2283
|
+
const failedWord = failedUsageCount === 1 ? "account" : "accounts";
|
|
2284
|
+
usageSpinner.stop(`Usage loaded (${failedUsageCount} ${failedWord} failed).`);
|
|
2285
|
+
}
|
|
2286
|
+
process.stdout.write("\nUsage:\n");
|
|
2287
|
+
for (let i = 0; i < status.accounts.length; i++) {
|
|
2288
|
+
const account = status.accounts[i];
|
|
2289
|
+
const marker = account.isCurrent ? "→ " : " ";
|
|
2290
|
+
const displayName = account.label ?? account.accountId;
|
|
2291
|
+
process.stdout.write(`${marker}${displayName}\n`);
|
|
2292
|
+
if (account.label) process.stdout.write(` ${account.accountId}\n`);
|
|
2293
|
+
const usageResult = usageResults[i];
|
|
2294
|
+
if (usageResult.status === "rejected") {
|
|
2295
|
+
const message = usageResult.reason instanceof Error ? usageResult.reason.message : "Fetch failed";
|
|
2296
|
+
process.stdout.write(` [usage unavailable] ${message}\n`);
|
|
2297
|
+
} else if (usageResult.value.ok) {
|
|
2298
|
+
const bars = formatUsageBars(usageResult.value.data);
|
|
2299
|
+
if (bars.length === 0) process.stdout.write(" Usage data unavailable\n");
|
|
1352
2300
|
for (const bar of bars) process.stdout.write(`${bar}\n`);
|
|
1353
|
-
}
|
|
2301
|
+
} else process.stdout.write(` [usage unavailable] ${usageResult.value.error.message}\n`);
|
|
1354
2302
|
if (i < status.accounts.length - 1) process.stdout.write("\n");
|
|
1355
2303
|
}
|
|
1356
|
-
const resolveLabel = (accountId) => {
|
|
1357
|
-
if (!accountId) return "unknown";
|
|
1358
|
-
return status.accounts.find((account) => account.accountId === accountId)?.label ?? accountId;
|
|
1359
|
-
};
|
|
1360
|
-
process.stdout.write("\nAuth files:\n");
|
|
1361
|
-
const ocStatus = status.opencodeAuth.exists ? `active: ${resolveLabel(status.opencodeAuth.accountId)}` : "not found";
|
|
1362
|
-
process.stdout.write(` OpenCode: ${ocStatus}\n`);
|
|
1363
|
-
const cxStatus = status.codexAuth.exists ? `active: ${resolveLabel(status.codexAuth.accountId)}` : "not found";
|
|
1364
|
-
process.stdout.write(` Codex CLI: ${cxStatus}\n`);
|
|
1365
|
-
const piStatus = status.piAuth.exists ? `active: ${resolveLabel(status.piAuth.accountId)}` : "not found";
|
|
1366
|
-
process.stdout.write(` Pi Agent: ${piStatus}\n`);
|
|
1367
2304
|
process.stdout.write("\n");
|
|
1368
2305
|
} catch (error) {
|
|
1369
2306
|
exitWithCommandError(error);
|
|
@@ -1378,7 +2315,7 @@ const switchNext = async () => {
|
|
|
1378
2315
|
const nextIndex = (config.current + 1) % config.accounts.length;
|
|
1379
2316
|
const nextAccount = config.accounts[nextIndex];
|
|
1380
2317
|
if (!nextAccount?.accountId) throw new Error("Account entry missing accountId.");
|
|
1381
|
-
const payload =
|
|
2318
|
+
const payload = await getSecretStoreAdapter().load(nextAccount.accountId);
|
|
1382
2319
|
const result = await writeAllAuthFiles(payload);
|
|
1383
2320
|
config.current = nextIndex;
|
|
1384
2321
|
await saveConfig(config);
|
|
@@ -1389,7 +2326,7 @@ const switchToAccount = async (identifier) => {
|
|
|
1389
2326
|
const index = config.accounts.findIndex((account) => account.accountId === identifier || account.label === identifier);
|
|
1390
2327
|
if (index === -1) throw new Error(`Account "${identifier}" not found. Use 'cdx login' to add it.`);
|
|
1391
2328
|
const account = config.accounts[index];
|
|
1392
|
-
const result = await writeAllAuthFiles(
|
|
2329
|
+
const result = await writeAllAuthFiles(await getSecretStoreAdapter().load(account.accountId));
|
|
1393
2330
|
config.current = index;
|
|
1394
2331
|
await saveConfig(config);
|
|
1395
2332
|
writeSwitchSummary(account.label ?? account.accountId, result);
|
|
@@ -1457,14 +2394,53 @@ const registerVersionCommand = (program, version) => {
|
|
|
1457
2394
|
//#endregion
|
|
1458
2395
|
//#region cdx.ts
|
|
1459
2396
|
const interactiveMode = runInteractiveMode;
|
|
2397
|
+
const parseSecretStoreSelection = (value) => {
|
|
2398
|
+
if (value === "auto" || value === "legacy-keychain") return value;
|
|
2399
|
+
throw new InvalidArgumentError(`Invalid value '${value}' for --secret-store. Allowed values: auto, legacy-keychain.`);
|
|
2400
|
+
};
|
|
2401
|
+
const getCompletionParseArgs = (argv) => {
|
|
2402
|
+
const completeIndex = argv.findIndex((arg) => arg === "complete");
|
|
2403
|
+
if (completeIndex === -1) return null;
|
|
2404
|
+
const separatorIndex = argv.findIndex((arg) => arg === "--");
|
|
2405
|
+
if (separatorIndex === -1 || separatorIndex <= completeIndex) return null;
|
|
2406
|
+
return argv.slice(separatorIndex + 1);
|
|
2407
|
+
};
|
|
2408
|
+
const getMacOSKeychainPromptWarning = (selection, platform = process.platform, backendId = null) => {
|
|
2409
|
+
if (platform !== "darwin") return null;
|
|
2410
|
+
if (selection === "legacy-keychain") return "⚠ macOS keychain is using the legacy security CLI backend. Touch ID may not be offered for keychain prompts.";
|
|
2411
|
+
if (selection === "auto" && backendId === "macos") return "⚠ macOS keychain is using the cross-keychain CLI fallback (`security`). Touch ID may not be offered for keychain prompts.";
|
|
2412
|
+
return null;
|
|
2413
|
+
};
|
|
2414
|
+
const maybeWarnAboutMacOSKeychainPromptMode = async (selection) => {
|
|
2415
|
+
let backendId = null;
|
|
2416
|
+
if (selection === "auto" && process.platform === "darwin") try {
|
|
2417
|
+
backendId = await resolveMacOSCrossKeychainBackendId();
|
|
2418
|
+
} catch {
|
|
2419
|
+
backendId = null;
|
|
2420
|
+
}
|
|
2421
|
+
const warning = getMacOSKeychainPromptWarning(selection, process.platform, backendId);
|
|
2422
|
+
if (warning) process.stderr.write(`${warning}\n`);
|
|
2423
|
+
};
|
|
1460
2424
|
const createProgram = (deps = {}) => {
|
|
1461
2425
|
const program = new Command();
|
|
1462
|
-
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(version, "-v, --version");
|
|
2426
|
+
program.name("cdx").description("OpenAI account switcher - manage multiple OpenAI Pro subscriptions").version(version, "-v, --version").option("--secret-store <mode>", "Select secret-store backend (auto|legacy-keychain)", parseSecretStoreSelection);
|
|
2427
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
2428
|
+
const options = actionCommand.optsWithGlobals();
|
|
2429
|
+
const configuredSelection = options.secretStore ? void 0 : await loadConfiguredSecretStoreSelection();
|
|
2430
|
+
const selection = options.secretStore ?? configuredSelection ?? "auto";
|
|
2431
|
+
setSecretStoreAdapter(createSecretStoreAdapterFromSelection(selection));
|
|
2432
|
+
await maybeWarnAboutMacOSKeychainPromptMode(selection);
|
|
2433
|
+
});
|
|
2434
|
+
program.hook("postAction", () => {
|
|
2435
|
+
resetSecretStoreAdapter();
|
|
2436
|
+
});
|
|
1463
2437
|
registerLoginCommand(program, deps);
|
|
1464
2438
|
registerReloginCommand(program);
|
|
1465
2439
|
registerSwitchCommand(program);
|
|
1466
2440
|
registerLabelCommand(program);
|
|
2441
|
+
registerMigrateSecretsCommand(program);
|
|
1467
2442
|
registerStatusCommand(program);
|
|
2443
|
+
registerDoctorCommand(program);
|
|
1468
2444
|
registerUsageCommand(program);
|
|
1469
2445
|
registerHelpCommand(program);
|
|
1470
2446
|
registerVersionCommand(program, version);
|
|
@@ -1472,11 +2448,18 @@ const createProgram = (deps = {}) => {
|
|
|
1472
2448
|
return program;
|
|
1473
2449
|
};
|
|
1474
2450
|
const main = async () => {
|
|
1475
|
-
|
|
2451
|
+
const program = createProgram();
|
|
2452
|
+
const completion = tab(program);
|
|
2453
|
+
const completionArgs = getCompletionParseArgs(process.argv);
|
|
2454
|
+
if (completionArgs) {
|
|
2455
|
+
completion.parse(completionArgs);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
await program.parseAsync(process.argv);
|
|
1476
2459
|
};
|
|
1477
2460
|
if (import.meta.main) main().catch((error) => {
|
|
1478
2461
|
exitWithCommandError(error);
|
|
1479
2462
|
});
|
|
1480
2463
|
|
|
1481
2464
|
//#endregion
|
|
1482
|
-
export { createProgram, createTestPaths, getPaths, interactiveMode, loadConfig, resetPaths, runInteractiveMode, saveConfig, setPaths, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };
|
|
2465
|
+
export { createProgram, createRuntimeSecretStoreAdapter, createSecretStoreAdapterFromSelection, createTestPaths, getMacOSKeychainPromptWarning, getPaths, getSecretStoreAdapter, interactiveMode, loadConfig, resetPaths, resetSecretStoreAdapter, resolveMacOSCrossKeychainBackendId, runInteractiveMode, saveConfig, setPaths, setSecretStoreAdapter, switchNext, switchToAccount, writeAllAuthFiles, writeAuthFile, writeCodexAuthFile, writePiAuthFile };
|