@andocorp/cli 0.1.3 → 0.3.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/LICENSE +21 -0
- package/README.md +95 -42
- package/dist/agent-commands.js +297 -0
- package/dist/api-command.js +187 -0
- package/dist/api-inputs.js +223 -0
- package/dist/api-operations.js +344 -0
- package/dist/args.js +71 -0
- package/dist/auth-commands.js +362 -0
- package/dist/cli-helpers.js +67 -0
- package/dist/cli-login-browser.js +60 -0
- package/dist/cli-login-errors.js +10 -0
- package/dist/cli-login-paths.js +8 -0
- package/dist/cli-login-revoke.js +100 -0
- package/dist/cli-login.js +335 -0
- package/dist/client.js +104 -0
- package/dist/commands.js +155 -0
- package/dist/config-credential-metadata.js +68 -0
- package/dist/config-keyring.js +61 -0
- package/dist/config-logout-credentials.js +171 -0
- package/dist/config-paths.js +41 -0
- package/dist/config-types.js +1 -0
- package/dist/config.js +333 -0
- package/dist/format.js +297 -0
- package/dist/help.js +70 -0
- package/dist/index.js +74 -11687
- package/dist/output.js +7 -0
- package/dist/session.js +58 -0
- package/dist/timeouts.js +1 -0
- package/dist/types.js +1 -0
- package/dist/watch-commands.js +120 -0
- package/package.json +24 -20
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const KEYRING_SERVICE = "ando-cli";
|
|
4
|
+
export async function readKeyringCredential(account, configRoot) {
|
|
5
|
+
const { Entry } = await loadKeyringModule();
|
|
6
|
+
try {
|
|
7
|
+
return new Entry(KEYRING_SERVICE, getKeyringAccount(configRoot, account)).getPassword();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new Error("Failed to read Ando credentials from the system keyring. Set ANDO_KEYRING=0 to use file-based auth instead.", { cause: error });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function writeKeyringCredential(account, apiKey, configRoot) {
|
|
14
|
+
const { Entry } = await loadKeyringModule();
|
|
15
|
+
try {
|
|
16
|
+
new Entry(KEYRING_SERVICE, getKeyringAccount(configRoot, account)).setPassword(apiKey);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error("Failed to save Ando credentials to the system keyring. Set ANDO_KEYRING=0 to use file-based auth instead.", { cause: error });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function deleteKeyringCredentialsForProfile(configRoot) {
|
|
23
|
+
const { Entry, findCredentials } = await loadKeyringModule();
|
|
24
|
+
const accountPrefix = `${getKeyringProfileId(configRoot)}:`;
|
|
25
|
+
let removed = false;
|
|
26
|
+
for (const credential of findCredentials(KEYRING_SERVICE)) {
|
|
27
|
+
if (!credential.account.startsWith(accountPrefix)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
removed = new Entry(KEYRING_SERVICE, credential.account).deletePassword() || removed;
|
|
31
|
+
}
|
|
32
|
+
return removed;
|
|
33
|
+
}
|
|
34
|
+
export async function readKeyringCredentialsForProfile(configRoot) {
|
|
35
|
+
const { findCredentials } = await loadKeyringModule();
|
|
36
|
+
const accountPrefix = `${getKeyringProfileId(configRoot)}:`;
|
|
37
|
+
return findCredentials(KEYRING_SERVICE)
|
|
38
|
+
.filter((credential) => {
|
|
39
|
+
return (credential.account.startsWith(accountPrefix) &&
|
|
40
|
+
typeof credential.password === "string" &&
|
|
41
|
+
credential.password.trim() !== "");
|
|
42
|
+
})
|
|
43
|
+
.map((credential) => ({
|
|
44
|
+
account: credential.account.slice(accountPrefix.length),
|
|
45
|
+
apiKey: credential.password,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
async function loadKeyringModule() {
|
|
49
|
+
try {
|
|
50
|
+
return await import("@napi-rs/keyring");
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new Error("Ando keyring storage is unavailable. Set ANDO_KEYRING=0 to use file-based auth instead.", { cause: error });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function getKeyringProfileId(configRoot) {
|
|
57
|
+
return createHash("sha256").update(path.resolve(configRoot)).digest("hex").slice(0, 32);
|
|
58
|
+
}
|
|
59
|
+
function getKeyringAccount(configRoot, account) {
|
|
60
|
+
return `${getKeyringProfileId(configRoot)}:${account}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getCredentialMetadataKey, readCredentialMetadataMap, readCredentialSource, } from "./config-credential-metadata.js";
|
|
4
|
+
import { readKeyringCredentialsForProfile } from "./config-keyring.js";
|
|
5
|
+
import { getAuthPath, getConfigReadPaths } from "./config-paths.js";
|
|
6
|
+
import { DEFAULT_CREDENTIAL_ACCOUNT, } from "./config-types.js";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function getNonEmptyString(source, key) {
|
|
11
|
+
const value = isRecord(source) ? source[key] : null;
|
|
12
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
13
|
+
}
|
|
14
|
+
function readConfigCredential(source) {
|
|
15
|
+
if (!isRecord(source) || !isRecord(source["credential"])) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const account = getNonEmptyString(source["credential"], "account");
|
|
19
|
+
const storage = getNonEmptyString(source["credential"], "storage");
|
|
20
|
+
const sourceType = readCredentialSource(source["credential"]);
|
|
21
|
+
return account == null || (storage !== "keyring" && storage !== "file")
|
|
22
|
+
? null
|
|
23
|
+
: { account, source: sourceType, storage };
|
|
24
|
+
}
|
|
25
|
+
function readConfigMetadata(parsed) {
|
|
26
|
+
const credential = readConfigCredential(parsed);
|
|
27
|
+
return {
|
|
28
|
+
apiHost: getNonEmptyString(parsed, "apiHost") ?? undefined,
|
|
29
|
+
baseUrl: getNonEmptyString(parsed, "baseUrl") ?? undefined,
|
|
30
|
+
credentialAccount: credential?.account,
|
|
31
|
+
credentialSource: credential?.source,
|
|
32
|
+
credentialStorage: credential?.storage,
|
|
33
|
+
defaultWorkspaceId: getNonEmptyString(parsed, "defaultWorkspaceId") ?? undefined,
|
|
34
|
+
defaultWorkspaceMembershipId: getNonEmptyString(parsed, "defaultWorkspaceMembershipId") ?? undefined,
|
|
35
|
+
realtimeHost: getNonEmptyString(parsed, "realtimeHost") ?? undefined,
|
|
36
|
+
savedCredentials: readCredentialMetadataMap(parsed),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function getKnownMetadata(configFile, credential) {
|
|
40
|
+
const metadata = readConfigMetadata(configFile.parsed);
|
|
41
|
+
const savedMetadata = metadata.savedCredentials.get(getCredentialMetadataKey(credential.storage, credential.account));
|
|
42
|
+
if (savedMetadata != null) {
|
|
43
|
+
return savedMetadata;
|
|
44
|
+
}
|
|
45
|
+
if (credential.account !== metadata.credentialAccount ||
|
|
46
|
+
credential.storage !== metadata.credentialStorage) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
apiHost: metadata.apiHost,
|
|
51
|
+
baseUrl: metadata.baseUrl,
|
|
52
|
+
credentialAccount: metadata.credentialAccount,
|
|
53
|
+
credentialStorage: metadata.credentialStorage,
|
|
54
|
+
defaultWorkspaceId: metadata.defaultWorkspaceId,
|
|
55
|
+
defaultWorkspaceMembershipId: metadata.defaultWorkspaceMembershipId,
|
|
56
|
+
realtimeHost: metadata.realtimeHost,
|
|
57
|
+
source: metadata.credentialSource,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function readConfigFile(configPath) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
63
|
+
return {
|
|
64
|
+
parsed: isRecord(parsed) ? parsed : null,
|
|
65
|
+
root: path.dirname(configPath),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return {
|
|
70
|
+
parsed: null,
|
|
71
|
+
root: path.dirname(configPath),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function readAuthFile(configRoot) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(await readFile(getAuthPath(configRoot), "utf8"));
|
|
78
|
+
return isRecord(parsed) ? parsed : null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readFileCredentials(authFile) {
|
|
85
|
+
const credentials = [];
|
|
86
|
+
if (authFile == null) {
|
|
87
|
+
return credentials;
|
|
88
|
+
}
|
|
89
|
+
if (typeof authFile.apiKey === "string" && authFile.apiKey.trim() !== "") {
|
|
90
|
+
credentials.push({
|
|
91
|
+
account: DEFAULT_CREDENTIAL_ACCOUNT,
|
|
92
|
+
apiKey: authFile.apiKey,
|
|
93
|
+
storage: "file",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (isRecord(authFile.credentials)) {
|
|
97
|
+
for (const [account, credential] of Object.entries(authFile.credentials)) {
|
|
98
|
+
if (isRecord(credential) && typeof credential["apiKey"] === "string") {
|
|
99
|
+
const apiKey = credential["apiKey"].trim();
|
|
100
|
+
if (apiKey !== "") {
|
|
101
|
+
credentials.push({ account, apiKey, storage: "file" });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return credentials;
|
|
107
|
+
}
|
|
108
|
+
async function readKeyringCredentials(configRoot) {
|
|
109
|
+
try {
|
|
110
|
+
return (await readKeyringCredentialsForProfile(configRoot)).map((credential) => ({
|
|
111
|
+
...credential,
|
|
112
|
+
storage: "keyring",
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function buildSavedConfig(configFile, credential) {
|
|
120
|
+
const metadata = getKnownMetadata(configFile, credential);
|
|
121
|
+
return {
|
|
122
|
+
apiKey: credential.apiKey,
|
|
123
|
+
apiHost: metadata?.apiHost,
|
|
124
|
+
baseUrl: metadata?.baseUrl,
|
|
125
|
+
credentialSource: metadata?.source,
|
|
126
|
+
defaultWorkspaceId: metadata?.defaultWorkspaceId,
|
|
127
|
+
defaultWorkspaceMembershipId: metadata?.defaultWorkspaceMembershipId,
|
|
128
|
+
realtimeHost: metadata?.realtimeHost,
|
|
129
|
+
revokeOnLogout: metadata?.source === "browser_login",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function readLegacyConfigCredential(configFile) {
|
|
133
|
+
const apiKey = getNonEmptyString(configFile.parsed, "apiKey");
|
|
134
|
+
return apiKey == null
|
|
135
|
+
? null
|
|
136
|
+
: {
|
|
137
|
+
apiKey,
|
|
138
|
+
apiHost: getNonEmptyString(configFile.parsed, "apiHost") ?? undefined,
|
|
139
|
+
baseUrl: getNonEmptyString(configFile.parsed, "baseUrl") ?? undefined,
|
|
140
|
+
defaultWorkspaceId: getNonEmptyString(configFile.parsed, "defaultWorkspaceId") ?? undefined,
|
|
141
|
+
defaultWorkspaceMembershipId: getNonEmptyString(configFile.parsed, "defaultWorkspaceMembershipId") ??
|
|
142
|
+
undefined,
|
|
143
|
+
realtimeHost: getNonEmptyString(configFile.parsed, "realtimeHost") ?? undefined,
|
|
144
|
+
revokeOnLogout: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
export async function readSavedConfigsForLogout() {
|
|
148
|
+
const configs = new Map();
|
|
149
|
+
function addConfig(config) {
|
|
150
|
+
const cacheKey = `${config.baseUrl ?? ""}\0${config.apiKey}`;
|
|
151
|
+
const existing = configs.get(cacheKey);
|
|
152
|
+
if (existing == null || (!existing.revokeOnLogout && config.revokeOnLogout)) {
|
|
153
|
+
configs.set(cacheKey, config);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const configPath of getConfigReadPaths()) {
|
|
157
|
+
const configFile = await readConfigFile(configPath);
|
|
158
|
+
const credentials = [
|
|
159
|
+
...(await readKeyringCredentials(configFile.root)),
|
|
160
|
+
...readFileCredentials(await readAuthFile(configFile.root)),
|
|
161
|
+
];
|
|
162
|
+
for (const credential of credentials) {
|
|
163
|
+
addConfig(buildSavedConfig(configFile, credential));
|
|
164
|
+
}
|
|
165
|
+
const legacyConfig = readLegacyConfigCredential(configFile);
|
|
166
|
+
if (legacyConfig != null) {
|
|
167
|
+
addConfig(legacyConfig);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...configs.values()];
|
|
171
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function getNonEmptyEnv(name) {
|
|
4
|
+
const value = process.env[name];
|
|
5
|
+
return value == null || value.trim() === "" ? null : value.trim();
|
|
6
|
+
}
|
|
7
|
+
function getConfigRootPath(platform = process.platform) {
|
|
8
|
+
const andoHome = getNonEmptyEnv("ANDO_HOME");
|
|
9
|
+
if (andoHome != null) {
|
|
10
|
+
return andoHome;
|
|
11
|
+
}
|
|
12
|
+
const xdgConfigHome = getNonEmptyEnv("XDG_CONFIG_HOME");
|
|
13
|
+
if (xdgConfigHome != null) {
|
|
14
|
+
return path.join(xdgConfigHome, "ando");
|
|
15
|
+
}
|
|
16
|
+
if (platform === "win32") {
|
|
17
|
+
return path.join(getNonEmptyEnv("APPDATA") ?? path.join(os.homedir(), "AppData", "Roaming"), "ando");
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), ".config", "ando");
|
|
20
|
+
}
|
|
21
|
+
function getLegacyConfigPath() {
|
|
22
|
+
return path.join(os.homedir(), ".config", "ando", "config.json");
|
|
23
|
+
}
|
|
24
|
+
export function getConfigReadPaths() {
|
|
25
|
+
const configPath = getConfigPath();
|
|
26
|
+
if (getNonEmptyEnv("ANDO_HOME") != null) {
|
|
27
|
+
return [configPath];
|
|
28
|
+
}
|
|
29
|
+
const legacyConfigPath = getLegacyConfigPath();
|
|
30
|
+
return configPath === legacyConfigPath ? [configPath] : [configPath, legacyConfigPath];
|
|
31
|
+
}
|
|
32
|
+
export function getConfigPath() {
|
|
33
|
+
return path.join(getConfigRootPath(), "config.json");
|
|
34
|
+
}
|
|
35
|
+
export function getAuthPath(configRoot = getConfigRootPath()) {
|
|
36
|
+
return path.join(configRoot, "auth.json");
|
|
37
|
+
}
|
|
38
|
+
export function isFileCredentialStorageEnabled() {
|
|
39
|
+
const value = getNonEmptyEnv("ANDO_KEYRING")?.toLowerCase();
|
|
40
|
+
return value === "0" || value === "false" || value === "off" || value === "no";
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_CREDENTIAL_ACCOUNT = "default";
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { deleteKeyringCredentialsForProfile, readKeyringCredential, writeKeyringCredential, } from "./config-keyring.js";
|
|
4
|
+
import { getAuthPath, getConfigPath, getConfigReadPaths, isFileCredentialStorageEnabled, } from "./config-paths.js";
|
|
5
|
+
import { DEFAULT_CREDENTIAL_ACCOUNT, } from "./config-types.js";
|
|
6
|
+
import { buildSavedConfigMetadata, getCredentialMetadataKey, getSaveCredentialMetadata, readCredentialMetadataMap, readCredentialSource, } from "./config-credential-metadata.js";
|
|
7
|
+
export { getConfigPath } from "./config-paths.js";
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function getNonEmptyString(source, key) {
|
|
12
|
+
const value = isRecord(source) ? source[key] : null;
|
|
13
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
14
|
+
}
|
|
15
|
+
function getErrorCode(error) {
|
|
16
|
+
return error instanceof Error && "code" in error ? error.code : undefined;
|
|
17
|
+
}
|
|
18
|
+
function buildConfigJsonFile(configPath, parsed, error) {
|
|
19
|
+
return { error, path: configPath, parsed, root: path.dirname(configPath) };
|
|
20
|
+
}
|
|
21
|
+
async function readJsonFile(pathname) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await readFile(pathname, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (getErrorCode(error) === "ENOENT") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readConfigJsonFile(options = {}) {
|
|
33
|
+
for (const configPath of getConfigReadPaths()) {
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = await readJsonFile(configPath);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (options.returnInvalid === true) {
|
|
40
|
+
return buildConfigJsonFile(configPath, null, error instanceof Error ? error.message : String(error));
|
|
41
|
+
}
|
|
42
|
+
if (options.ignoreInvalid === true) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
if (parsed == null) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
return buildConfigJsonFile(configPath, parsed);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
async function readConfigJsonFiles() {
|
|
55
|
+
return await Promise.all(getConfigReadPaths().map(readConfigJsonFileForCleanup));
|
|
56
|
+
}
|
|
57
|
+
async function readConfigJsonFileForCleanup(configPath) {
|
|
58
|
+
try {
|
|
59
|
+
return buildConfigJsonFile(configPath, await readJsonFile(configPath));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return buildConfigJsonFile(configPath, null, error instanceof Error ? error.message : String(error));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function readCredentialConfig(source) {
|
|
66
|
+
if (!isRecord(source) || !isRecord(source["credential"])) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const account = getNonEmptyString(source["credential"], "account");
|
|
70
|
+
const storage = getNonEmptyString(source["credential"], "storage");
|
|
71
|
+
if (account == null || (storage !== "keyring" && storage !== "file")) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
account,
|
|
76
|
+
source: readCredentialSource(source["credential"]),
|
|
77
|
+
storage,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function readKeyringAccounts(source) {
|
|
81
|
+
const accounts = new Set();
|
|
82
|
+
if (isRecord(source) && Array.isArray(source["keyringAccounts"])) {
|
|
83
|
+
for (const account of source["keyringAccounts"]) {
|
|
84
|
+
if (typeof account === "string" && account.trim() !== "") {
|
|
85
|
+
accounts.add(account.trim());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const credential = readCredentialConfig(source);
|
|
90
|
+
if (credential?.storage === "keyring") {
|
|
91
|
+
accounts.add(DEFAULT_CREDENTIAL_ACCOUNT);
|
|
92
|
+
accounts.add(credential.account);
|
|
93
|
+
}
|
|
94
|
+
return [...accounts].sort();
|
|
95
|
+
}
|
|
96
|
+
function getSaveCredentialConfig(config) {
|
|
97
|
+
return {
|
|
98
|
+
account: config.defaultWorkspaceId ?? DEFAULT_CREDENTIAL_ACCOUNT,
|
|
99
|
+
source: config.credentialSource,
|
|
100
|
+
storage: isFileCredentialStorageEnabled() ? "file" : "keyring",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function getReadCredentialConfig(parsed) {
|
|
104
|
+
const credential = readCredentialConfig(parsed);
|
|
105
|
+
if (credential == null) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...credential,
|
|
110
|
+
storage: isFileCredentialStorageEnabled() ? "file" : credential.storage,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function readAuthFile(configRoot) {
|
|
114
|
+
const parsed = await readJsonFile(getAuthPath(configRoot));
|
|
115
|
+
return isRecord(parsed) ? parsed : null;
|
|
116
|
+
}
|
|
117
|
+
function readFileCredentialFromAuth(authFile, account) {
|
|
118
|
+
if (authFile == null) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const credential = authFile.credentials?.[account];
|
|
122
|
+
if (credential != null && typeof credential.apiKey === "string") {
|
|
123
|
+
return credential.apiKey.trim() === "" ? null : credential.apiKey;
|
|
124
|
+
}
|
|
125
|
+
return typeof authFile.apiKey === "string" && authFile.apiKey.trim() !== ""
|
|
126
|
+
? authFile.apiKey
|
|
127
|
+
: null;
|
|
128
|
+
}
|
|
129
|
+
async function readFileCredential(account, configRoot) {
|
|
130
|
+
return readFileCredentialFromAuth(await readAuthFile(configRoot), account);
|
|
131
|
+
}
|
|
132
|
+
async function writeFileCredential(account, apiKey, configRoot) {
|
|
133
|
+
const authPath = getAuthPath(configRoot);
|
|
134
|
+
const savedAt = new Date().toISOString();
|
|
135
|
+
const existing = await readAuthFile(configRoot);
|
|
136
|
+
const credentials = isRecord(existing?.credentials)
|
|
137
|
+
? Object.fromEntries(Object.entries(existing.credentials).filter((entry) => {
|
|
138
|
+
return isRecord(entry[1]) && typeof entry[1]["apiKey"] === "string";
|
|
139
|
+
}))
|
|
140
|
+
: {};
|
|
141
|
+
credentials[account] = { apiKey, savedAt };
|
|
142
|
+
await mkdir(path.dirname(authPath), { recursive: true });
|
|
143
|
+
await writeFile(authPath, JSON.stringify({ credentials, savedAt, version: 1 }, null, 2), { mode: 0o600 });
|
|
144
|
+
}
|
|
145
|
+
async function deleteFileCredential(account, configRoot) {
|
|
146
|
+
const authPath = getAuthPath(configRoot);
|
|
147
|
+
let existing;
|
|
148
|
+
try {
|
|
149
|
+
existing = await readAuthFile(configRoot);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
await rm(authPath, { force: true });
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (existing == null) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const credentials = isRecord(existing.credentials)
|
|
159
|
+
? Object.fromEntries(Object.entries(existing.credentials).filter((entry) => {
|
|
160
|
+
return (entry[0] !== account &&
|
|
161
|
+
isRecord(entry[1]) &&
|
|
162
|
+
typeof entry[1]["apiKey"] === "string");
|
|
163
|
+
}))
|
|
164
|
+
: {};
|
|
165
|
+
const hasOtherCredentials = Object.keys(credentials).length > 0;
|
|
166
|
+
if (!hasOtherCredentials) {
|
|
167
|
+
await rm(authPath, { force: true });
|
|
168
|
+
return readFileCredentialFromAuth(existing, account) != null;
|
|
169
|
+
}
|
|
170
|
+
await writeFile(authPath, JSON.stringify({ credentials, savedAt: new Date().toISOString(), version: 1 }, null, 2), { mode: 0o600 });
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
async function readCredential(credential, configRoot) {
|
|
174
|
+
if (credential == null) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return credential.storage === "file"
|
|
178
|
+
? await readFileCredential(credential.account, configRoot)
|
|
179
|
+
: await readKeyringCredential(credential.account, configRoot);
|
|
180
|
+
}
|
|
181
|
+
async function writeCredential(credential, apiKey, configRoot) {
|
|
182
|
+
if (credential.storage === "file") {
|
|
183
|
+
await writeFileCredential(credential.account, apiKey, configRoot);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await writeKeyringCredential(credential.account, apiKey, configRoot);
|
|
187
|
+
}
|
|
188
|
+
async function deleteKeyringCredentials(configRoot, required) {
|
|
189
|
+
try {
|
|
190
|
+
return await deleteKeyringCredentialsForProfile(configRoot);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (required) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function deleteCredentials(parsed, configRoot, forceKeyringCleanup = false) {
|
|
200
|
+
const credential = readCredentialConfig(parsed);
|
|
201
|
+
let removedCredential = false;
|
|
202
|
+
removedCredential =
|
|
203
|
+
(await deleteKeyringCredentials(configRoot, forceKeyringCleanup || credential?.storage === "keyring")) || removedCredential;
|
|
204
|
+
if (credential?.storage === "file") {
|
|
205
|
+
removedCredential =
|
|
206
|
+
(await deleteFileCredential(credential.account, configRoot)) || removedCredential;
|
|
207
|
+
}
|
|
208
|
+
return removedCredential;
|
|
209
|
+
}
|
|
210
|
+
function setOptionalString(payload, key, value) {
|
|
211
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
212
|
+
payload[key] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function readSavedConfig() {
|
|
216
|
+
const configFile = await readConfigJsonFile();
|
|
217
|
+
if (configFile == null || !isRecord(configFile.parsed)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const credential = getReadCredentialConfig(configFile.parsed);
|
|
221
|
+
const storedApiKey = await readCredential(credential, configFile.root);
|
|
222
|
+
const legacyApiKey = getNonEmptyString(configFile.parsed, "apiKey");
|
|
223
|
+
const apiKey = storedApiKey ?? legacyApiKey;
|
|
224
|
+
if (apiKey == null) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
apiKey,
|
|
229
|
+
...buildSavedConfigMetadata(configFile.parsed),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
export async function readSavedConfigMetadata() {
|
|
233
|
+
const configFile = await readConfigJsonFile();
|
|
234
|
+
return configFile != null && isRecord(configFile.parsed)
|
|
235
|
+
? buildSavedConfigMetadata(configFile.parsed)
|
|
236
|
+
: null;
|
|
237
|
+
}
|
|
238
|
+
export async function inspectSavedConfig() {
|
|
239
|
+
const configFile = await readConfigJsonFile({ returnInvalid: true });
|
|
240
|
+
const configPath = configFile?.path ?? getConfigPath();
|
|
241
|
+
const configRoot = configFile?.root ?? path.dirname(configPath);
|
|
242
|
+
const authPath = getAuthPath(configRoot);
|
|
243
|
+
if (configFile == null || !isRecord(configFile.parsed)) {
|
|
244
|
+
return {
|
|
245
|
+
authPath,
|
|
246
|
+
configError: configFile?.error,
|
|
247
|
+
configPath,
|
|
248
|
+
credentialError: configFile?.error,
|
|
249
|
+
hasConfig: configFile != null,
|
|
250
|
+
hasCredential: false,
|
|
251
|
+
hasLegacyCredential: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const credential = getReadCredentialConfig(configFile.parsed);
|
|
255
|
+
const legacyApiKey = getNonEmptyString(configFile.parsed, "apiKey");
|
|
256
|
+
let hasCredential = false;
|
|
257
|
+
let credentialError;
|
|
258
|
+
if (credential != null) {
|
|
259
|
+
try {
|
|
260
|
+
hasCredential = (await readCredential(credential, configRoot)) != null;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
credentialError = error instanceof Error ? error.message : String(error);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
authPath,
|
|
268
|
+
configPath,
|
|
269
|
+
configError: configFile.error,
|
|
270
|
+
credentialAccount: credential?.account,
|
|
271
|
+
credentialError,
|
|
272
|
+
credentialStorage: credential?.storage,
|
|
273
|
+
hasConfig: true,
|
|
274
|
+
hasCredential,
|
|
275
|
+
hasLegacyCredential: legacyApiKey != null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export async function clearSavedConfig() {
|
|
279
|
+
const targets = await readConfigJsonFiles();
|
|
280
|
+
let removedCredential = false;
|
|
281
|
+
for (const configFile of targets) {
|
|
282
|
+
removedCredential =
|
|
283
|
+
(await deleteCredentials(configFile.parsed, configFile.root, configFile.error != null)) || removedCredential;
|
|
284
|
+
await rm(configFile.path, { force: true });
|
|
285
|
+
await rm(getAuthPath(configFile.root), { force: true });
|
|
286
|
+
}
|
|
287
|
+
const firstTarget = targets[0] ?? {
|
|
288
|
+
path: getConfigPath(),
|
|
289
|
+
root: path.dirname(getConfigPath()),
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
authPath: getAuthPath(firstTarget.root),
|
|
293
|
+
configPath: firstTarget.path,
|
|
294
|
+
removedCredential,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export async function saveConfig(config) {
|
|
298
|
+
const configPath = getConfigPath();
|
|
299
|
+
const configRoot = path.dirname(configPath);
|
|
300
|
+
const savedAt = new Date().toISOString();
|
|
301
|
+
const credential = getSaveCredentialConfig(config);
|
|
302
|
+
const existingConfig = await readConfigJsonFile({ ignoreInvalid: true });
|
|
303
|
+
const keyringAccounts = new Set(existingConfig?.root === configRoot ? readKeyringAccounts(existingConfig.parsed) : []);
|
|
304
|
+
const credentialMetadata = existingConfig?.root === configRoot
|
|
305
|
+
? readCredentialMetadataMap(existingConfig.parsed)
|
|
306
|
+
: new Map();
|
|
307
|
+
if (credential.storage === "keyring") {
|
|
308
|
+
keyringAccounts.add(DEFAULT_CREDENTIAL_ACCOUNT);
|
|
309
|
+
keyringAccounts.add(credential.account);
|
|
310
|
+
}
|
|
311
|
+
credentialMetadata.set(getCredentialMetadataKey(credential.storage, credential.account), getSaveCredentialMetadata(config));
|
|
312
|
+
await writeCredential(credential, config.apiKey, configRoot);
|
|
313
|
+
const payload = {
|
|
314
|
+
credential,
|
|
315
|
+
savedAt,
|
|
316
|
+
version: 1,
|
|
317
|
+
};
|
|
318
|
+
if (keyringAccounts.size > 0) {
|
|
319
|
+
payload.keyringAccounts = [...keyringAccounts].sort();
|
|
320
|
+
}
|
|
321
|
+
if (credentialMetadata.size > 0) {
|
|
322
|
+
payload.credentialMetadata = Object.fromEntries(credentialMetadata);
|
|
323
|
+
}
|
|
324
|
+
setOptionalString(payload, "apiHost", config.apiHost);
|
|
325
|
+
setOptionalString(payload, "baseUrl", config.baseUrl);
|
|
326
|
+
setOptionalString(payload, "defaultWorkspaceId", config.defaultWorkspaceId);
|
|
327
|
+
setOptionalString(payload, "defaultWorkspaceMembershipId", config.defaultWorkspaceMembershipId);
|
|
328
|
+
setOptionalString(payload, "realtimeHost", config.realtimeHost);
|
|
329
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
330
|
+
await writeFile(configPath, JSON.stringify(payload, null, 2), {
|
|
331
|
+
mode: 0o600,
|
|
332
|
+
});
|
|
333
|
+
}
|