@andocorp/cli 0.2.0 → 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.
@@ -0,0 +1,335 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { sleep } from "./cli-login-browser.js";
4
+ import { CliLoginRequestError } from "./cli-login-errors.js";
5
+ import { getPendingCliLoginPath, getPendingCliLoginPaths } from "./cli-login-paths.js";
6
+ import { DEFAULT_CLI_REQUEST_TIMEOUT_MS } from "./timeouts.js";
7
+ const CREATE_CLI_LOGIN_SESSION_PATH = "/auth/cli-login/session";
8
+ const REDEEM_CLI_LOGIN_SESSION_PATH = "/auth/cli-login/redeem";
9
+ const DEFAULT_CLI_LOGIN_POLL_INTERVAL_MS = 2_000;
10
+ const terminalCliLoginErrorCodes = new Set([
11
+ "cli_login_already_approved",
12
+ "cli_login_expired",
13
+ "cli_login_invalid_poll_token",
14
+ "cli_login_membership_inactive",
15
+ "cli_login_session_consumed",
16
+ "cli_login_session_not_found",
17
+ "invalid_request",
18
+ "invalid_api_key",
19
+ "unsupported_api_key_type",
20
+ ]);
21
+ const terminalCliLoginErrorMessages = {
22
+ cli_login_already_approved: "CLI login was already approved. Run `ando login` again to start a new session.",
23
+ cli_login_expired: "CLI login session expired. Run `ando login` again.",
24
+ cli_login_invalid_poll_token: "CLI login session is invalid. Run `ando login` again.",
25
+ cli_login_membership_inactive: "Your Ando workspace membership is inactive, so CLI login cannot continue.",
26
+ cli_login_rate_limited: "CLI login is rate-limited. Try again later.",
27
+ cli_login_session_consumed: "CLI login session was already consumed. Run `ando login` again.",
28
+ cli_login_session_not_found: "CLI login session was not found. Run `ando login` again.",
29
+ invalid_api_key: "CLI credential is no longer valid.",
30
+ invalid_request: "CLI login request was invalid. Run `ando login` again.",
31
+ unsupported_api_key_type: "This API key cannot be revoked by the CLI.",
32
+ };
33
+ export { openBrowser, sleep } from "./cli-login-browser.js";
34
+ export { CliLoginRequestError } from "./cli-login-errors.js";
35
+ export async function savePendingCliLoginSession(session) {
36
+ const pendingPath = getPendingCliLoginPath();
37
+ await mkdir(path.dirname(pendingPath), { recursive: true });
38
+ const payload = {
39
+ ...session,
40
+ savedAt: new Date().toISOString(),
41
+ };
42
+ await writeFile(pendingPath, JSON.stringify(payload, null, 2), {
43
+ mode: 0o600,
44
+ });
45
+ }
46
+ export async function readPendingCliLoginSession() {
47
+ try {
48
+ const raw = await readFile(getPendingCliLoginPath(), "utf8");
49
+ const parsed = JSON.parse(raw);
50
+ return parsePendingCliLoginSession(parsed);
51
+ }
52
+ catch (error) {
53
+ const errorCode = error instanceof Error && "code" in error ? error.code : undefined;
54
+ if (errorCode === "ENOENT") {
55
+ return null;
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+ export async function clearPendingCliLoginSession() {
61
+ await Promise.all(getPendingCliLoginPaths().map((pendingPath) => rm(pendingPath, { force: true })));
62
+ }
63
+ export async function createCliLoginSession(params) {
64
+ const response = await postCliLoginJson({
65
+ baseUrl: params.baseUrl,
66
+ fetchFn: params.fetchFn,
67
+ path: CREATE_CLI_LOGIN_SESSION_PATH,
68
+ });
69
+ if (response.status !== 201) {
70
+ throwCliLoginResponseError(response.body);
71
+ }
72
+ return parseCreateCliLoginSessionResponse(response.body);
73
+ }
74
+ export async function redeemCliLoginSession(params) {
75
+ const response = await postCliLoginJson({
76
+ baseUrl: params.baseUrl,
77
+ body: {
78
+ cli_auth_session_id: params.cliAuthSessionId,
79
+ poll_token: params.pollToken,
80
+ },
81
+ fetchFn: params.fetchFn,
82
+ path: REDEEM_CLI_LOGIN_SESSION_PATH,
83
+ });
84
+ if (response.status === 202) {
85
+ return parseRedeemCliLoginSessionPendingResponse(response, params.now);
86
+ }
87
+ if (response.status === 200) {
88
+ return parseRedeemCliLoginSessionSuccessResponse(response.body);
89
+ }
90
+ throwCliLoginResponseError(response.body);
91
+ }
92
+ export async function pollCliLoginSession(params) {
93
+ const redeemSession = params.redeemSession ?? redeemCliLoginSession;
94
+ const sleepFn = params.sleep ?? sleep;
95
+ const now = params.now ?? Date.now;
96
+ let expiresAt = params.expiresAt;
97
+ while (now() < parseCliLoginExpirationMs(expiresAt)) {
98
+ const result = await redeemSession({
99
+ baseUrl: params.baseUrl,
100
+ cliAuthSessionId: params.cliAuthSessionId,
101
+ fetchFn: params.fetchFn,
102
+ now,
103
+ pollToken: params.pollToken,
104
+ });
105
+ if (result.status === "approved") {
106
+ return result;
107
+ }
108
+ expiresAt = result.expiresAt;
109
+ const remainingMs = parseCliLoginExpirationMs(expiresAt) - now();
110
+ if (remainingMs <= 0) {
111
+ break;
112
+ }
113
+ await sleepFn(Math.min(result.retryAfterMs, remainingMs));
114
+ }
115
+ throw new CliLoginRequestError("CLI login session expired. Run `ando login` again.", {
116
+ errorCode: "cli_login_expired",
117
+ terminal: true,
118
+ });
119
+ }
120
+ function buildCliLoginUrl(baseUrl, apiPath) {
121
+ return `${baseUrl.replace(/\/+$/, "")}${apiPath}`;
122
+ }
123
+ async function postCliLoginJson(params) {
124
+ const controller = new AbortController();
125
+ const timeout = setTimeout(() => {
126
+ controller.abort();
127
+ }, DEFAULT_CLI_REQUEST_TIMEOUT_MS);
128
+ try {
129
+ const init = {
130
+ headers: {
131
+ Accept: "application/json",
132
+ ...(params.body == null ? {} : { "Content-Type": "application/json" }),
133
+ },
134
+ method: "POST",
135
+ signal: controller.signal,
136
+ };
137
+ if (params.body != null) {
138
+ init.body = JSON.stringify(params.body);
139
+ }
140
+ const fetchFn = params.fetchFn ?? globalThis.fetch.bind(globalThis);
141
+ const response = await fetchFn(buildCliLoginUrl(params.baseUrl, params.path), init);
142
+ const bodyText = await response.text();
143
+ return {
144
+ body: bodyText.trim() === "" ? null : parseCliLoginJson(bodyText),
145
+ headers: response.headers,
146
+ status: response.status,
147
+ };
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error && error.name === "AbortError") {
151
+ throw new Error(`[ando-cli] login request timed out after ${DEFAULT_CLI_REQUEST_TIMEOUT_MS}ms`, { cause: error });
152
+ }
153
+ throw error;
154
+ }
155
+ finally {
156
+ clearTimeout(timeout);
157
+ }
158
+ }
159
+ function parseCliLoginJson(bodyText) {
160
+ try {
161
+ return JSON.parse(bodyText);
162
+ }
163
+ catch {
164
+ throw new CliLoginRequestError("Malformed CLI login response: invalid JSON.", {
165
+ terminal: true,
166
+ });
167
+ }
168
+ }
169
+ function parseCreateCliLoginSessionResponse(body) {
170
+ const response = getRecord(body);
171
+ const data = getRecord(response["data"]);
172
+ if (response["success"] !== true) {
173
+ throwMalformedCliLoginResponse("create session response missing data");
174
+ }
175
+ const browserUrl = getNonEmptyString(data, "browser_url");
176
+ const cliAuthSessionId = getNonEmptyString(data, "cli_auth_session_id");
177
+ const expiresAt = getCliLoginExpiresAt(data, "expires_at");
178
+ const pollToken = getNonEmptyString(data, "poll_token");
179
+ const verificationCode = getNonEmptyString(data, "verification_code");
180
+ if (browserUrl == null ||
181
+ cliAuthSessionId == null ||
182
+ expiresAt == null ||
183
+ pollToken == null ||
184
+ verificationCode == null) {
185
+ throwMalformedCliLoginResponse("create session response missing required fields");
186
+ }
187
+ parseCliLoginExpirationMs(expiresAt);
188
+ return {
189
+ browserUrl,
190
+ cliAuthSessionId,
191
+ expiresAt,
192
+ pollToken,
193
+ verificationCode,
194
+ };
195
+ }
196
+ function parseRedeemCliLoginSessionPendingResponse(response, now) {
197
+ const body = getRecord(response.body);
198
+ const expiresAt = getCliLoginExpiresAt(body, "expires_at");
199
+ const status = getNonEmptyString(body, "status");
200
+ const errorCode = getNonEmptyString(body, "error_code");
201
+ if (expiresAt == null ||
202
+ status !== "pending" ||
203
+ errorCode !== "authorization_pending") {
204
+ throwMalformedCliLoginResponse("pending redeem response missing required fields");
205
+ }
206
+ parseCliLoginExpirationMs(expiresAt);
207
+ return {
208
+ status: "pending",
209
+ expiresAt,
210
+ retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After"), now) ??
211
+ DEFAULT_CLI_LOGIN_POLL_INTERVAL_MS,
212
+ };
213
+ }
214
+ function parseRedeemCliLoginSessionSuccessResponse(body) {
215
+ const response = getRecord(body);
216
+ const data = getRecord(response["data"]);
217
+ if (response["success"] !== true) {
218
+ throwMalformedCliLoginResponse("success redeem response missing data");
219
+ }
220
+ const status = getNonEmptyString(data, "status");
221
+ const apiKey = getNonEmptyString(data, "api_key");
222
+ if (status !== "approved" || apiKey == null) {
223
+ throwMalformedCliLoginResponse("success redeem response missing approval data");
224
+ }
225
+ const workspaceId = getNonEmptyString(data, "workspace_id");
226
+ const workspaceMembershipId = getNonEmptyString(data, "workspace_membership_id");
227
+ return {
228
+ status: "approved",
229
+ apiKey,
230
+ workspaceId: workspaceId ?? undefined,
231
+ workspaceMembershipId: workspaceMembershipId ?? undefined,
232
+ };
233
+ }
234
+ function parsePendingCliLoginSession(parsed) {
235
+ const apiHost = getRequiredPendingString(parsed, "apiHost");
236
+ const baseUrl = getRequiredPendingString(parsed, "baseUrl");
237
+ const browserUrl = getRequiredPendingString(parsed, "browserUrl");
238
+ const cliAuthSessionId = getRequiredPendingString(parsed, "cliAuthSessionId");
239
+ const expiresAt = getRequiredPendingExpiresAt(parsed, "expiresAt");
240
+ const pollToken = getRequiredPendingString(parsed, "pollToken");
241
+ const realtimeHost = typeof parsed.realtimeHost === "string" ? parsed.realtimeHost : undefined;
242
+ const verificationCode = getRequiredPendingString(parsed, "verificationCode");
243
+ parseCliLoginExpirationMs(expiresAt);
244
+ return {
245
+ apiHost,
246
+ baseUrl,
247
+ browserUrl,
248
+ cliAuthSessionId,
249
+ expiresAt,
250
+ pollToken,
251
+ realtimeHost,
252
+ verificationCode,
253
+ };
254
+ }
255
+ function getRequiredPendingString(parsed, key) {
256
+ const value = parsed[key];
257
+ if (typeof value === "string" && value.trim() !== "") {
258
+ return value;
259
+ }
260
+ throw new CliLoginRequestError("Pending CLI login session is malformed. Run `ando login --no-browser` again.", {
261
+ terminal: true,
262
+ });
263
+ }
264
+ function getRequiredPendingExpiresAt(parsed, key) {
265
+ const value = parsed[key];
266
+ if ((typeof value === "string" && value.trim() !== "") ||
267
+ (typeof value === "number" && Number.isFinite(value))) {
268
+ return value;
269
+ }
270
+ throw new CliLoginRequestError("Pending CLI login session is malformed. Run `ando login --no-browser` again.", {
271
+ terminal: true,
272
+ });
273
+ }
274
+ function parseRetryAfterMs(value, now) {
275
+ if (value == null || value.trim() === "") {
276
+ return undefined;
277
+ }
278
+ const numericSeconds = Number(value);
279
+ if (Number.isFinite(numericSeconds) && numericSeconds >= 0) {
280
+ return numericSeconds * 1_000;
281
+ }
282
+ const retryAtMs = Date.parse(value);
283
+ if (Number.isFinite(retryAtMs)) {
284
+ return Math.max(0, retryAtMs - (now ?? Date.now)());
285
+ }
286
+ return undefined;
287
+ }
288
+ function parseCliLoginExpirationMs(expiresAt) {
289
+ const expiresAtMs = typeof expiresAt === "number" ? expiresAt : Date.parse(expiresAt);
290
+ if (!Number.isFinite(expiresAtMs)) {
291
+ throwMalformedCliLoginResponse("expires_at is not a valid timestamp");
292
+ }
293
+ return expiresAtMs;
294
+ }
295
+ function throwCliLoginResponseError(body) {
296
+ const response = getRecord(body);
297
+ const errorCode = getNonEmptyString(response, "error_code");
298
+ const errorMessage = getNonEmptyString(response, "error_message");
299
+ if (errorCode == null) {
300
+ throwMalformedCliLoginResponse("error response missing error_code");
301
+ }
302
+ throw new CliLoginRequestError(formatCliLoginError(errorCode, errorMessage), {
303
+ errorCode,
304
+ terminal: terminalCliLoginErrorCodes.has(errorCode),
305
+ });
306
+ }
307
+ function formatCliLoginError(errorCode, errorMessage) {
308
+ const knownMessage = terminalCliLoginErrorMessages[errorCode];
309
+ const message = knownMessage ?? errorMessage ?? "CLI login failed.";
310
+ return `${message} (${errorCode})`;
311
+ }
312
+ function throwMalformedCliLoginResponse(reason) {
313
+ throw new CliLoginRequestError(`Malformed CLI login response: ${reason}.`, {
314
+ terminal: true,
315
+ });
316
+ }
317
+ function getRecord(value) {
318
+ return value != null && typeof value === "object" && !Array.isArray(value)
319
+ ? value
320
+ : {};
321
+ }
322
+ function getNonEmptyString(record, key) {
323
+ const value = record[key];
324
+ return typeof value === "string" && value.trim() !== "" ? value : null;
325
+ }
326
+ function getCliLoginExpiresAt(record, key) {
327
+ const value = record[key];
328
+ if (typeof value === "string" && value.trim() !== "") {
329
+ return value;
330
+ }
331
+ if (typeof value === "number" && Number.isFinite(value)) {
332
+ return value;
333
+ }
334
+ return null;
335
+ }
package/dist/client.js ADDED
@@ -0,0 +1,104 @@
1
+ import { AndoClient, } from "@andocorp/sdk";
2
+ import { DEFAULT_CLI_REQUEST_TIMEOUT_MS } from "./timeouts.js";
3
+ function adaptConversationPage(response) {
4
+ return {
5
+ items: response.items,
6
+ cursor: response.pageInfo.hasPreviousPage
7
+ ? (response.pageInfo.startCursor ?? null)
8
+ : null,
9
+ hasMore: response.pageInfo.hasPreviousPage,
10
+ };
11
+ }
12
+ function formatDiagnosticError(error) {
13
+ return error instanceof Error ? error.message : String(error);
14
+ }
15
+ export function createCliDiagnosticsSink() {
16
+ const debug = process.env["ANDO_DEBUG"];
17
+ if (debug !== "1" && debug !== "true") {
18
+ return undefined;
19
+ }
20
+ return (event) => {
21
+ if (event.type === "http") {
22
+ const status = event.status == null ? "failed" : String(event.status);
23
+ const suffix = event.ok ? "" : ` ${formatDiagnosticError(event.error)}`;
24
+ process.stderr.write(`[ando-debug] ${event.method} ${event.path} ${status} ${event.durationMs}ms${suffix}\n`);
25
+ return;
26
+ }
27
+ const suffix = event.error == null ? "" : ` ${formatDiagnosticError(event.error)}`;
28
+ process.stderr.write(`[ando-debug] realtime ${event.operation} ${event.reason ?? ""} ${event.durationMs ?? 0}ms${suffix}\n`);
29
+ };
30
+ }
31
+ export function createAndoCliClient(config) {
32
+ const client = new AndoClient({
33
+ auth: {
34
+ apiKey: config.apiKey,
35
+ },
36
+ baseUrl: config.baseUrl,
37
+ diagnostics: createCliDiagnosticsSink(),
38
+ realtimeHost: config.realtimeHost,
39
+ requestTimeoutMs: DEFAULT_CLI_REQUEST_TIMEOUT_MS,
40
+ });
41
+ async function getAllMemberships() {
42
+ const memberships = [];
43
+ let cursor;
44
+ do {
45
+ const response = await client.getMyMemberships({
46
+ cursor,
47
+ limit: 200,
48
+ });
49
+ memberships.push(...response.items);
50
+ cursor = response.pageInfo.hasNextPage ? response.pageInfo.nextCursor : undefined;
51
+ } while (cursor != null);
52
+ return memberships;
53
+ }
54
+ async function getConversationMessagesPage(params) {
55
+ const query = {
56
+ ...(params.cursor != null ? { before: params.cursor } : {}),
57
+ limit: params.limit ?? 25,
58
+ };
59
+ return adaptConversationPage(await client.getConversationMessages(params.conversationId, query));
60
+ }
61
+ async function getMessage(messageId) {
62
+ try {
63
+ return await client.getConversationMessage(messageId);
64
+ }
65
+ catch (error) {
66
+ if (error instanceof Error && error.message.includes("404")) {
67
+ return null;
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+ return {
73
+ addReaction(messageId, emoji) {
74
+ return client.addReaction(messageId, emoji);
75
+ },
76
+ async close() { },
77
+ getAllMemberships,
78
+ getConversationMessages(conversationId, limit = 25) {
79
+ return getConversationMessagesPage({
80
+ conversationId,
81
+ limit,
82
+ });
83
+ },
84
+ getConversationMessagesPage,
85
+ getMe() {
86
+ return client.getMe();
87
+ },
88
+ getMessage,
89
+ async getThreadReplies(messageId) {
90
+ const response = await client.getThreadReplies(messageId, {
91
+ limit: 100,
92
+ });
93
+ return response.items;
94
+ },
95
+ postMessage(params) {
96
+ return client.postMessage({
97
+ conversationId: params.conversationId,
98
+ markdownContent: params.markdownContent,
99
+ threadRootId: params.threadRootId,
100
+ });
101
+ },
102
+ realtime: client.realtime,
103
+ };
104
+ }
@@ -0,0 +1,155 @@
1
+ import { DEFAULT_ANDO_BASE_URL } from "@andocorp/sdk";
2
+ import { getStringFlag, hasFlag } from "./args.js";
3
+ import { CliLoginRequestError, clearPendingCliLoginSession, createCliLoginSession, openBrowser, pollCliLoginSession, readPendingCliLoginSession, savePendingCliLoginSession, } from "./cli-login.js";
4
+ import { getConfigPath, readSavedConfigMetadata, saveConfig } from "./config.js";
5
+ import { getConfiguredApiHost, getConfiguredBaseUrl, getConfiguredRealtimeHost, } from "./session.js";
6
+ export async function runLogin(parsedArgs, dependencies = {}) {
7
+ const loginSubcommand = parsedArgs.positionals[1];
8
+ if (loginSubcommand === "poll") {
9
+ await runLoginPoll(dependencies);
10
+ return;
11
+ }
12
+ if (loginSubcommand != null) {
13
+ throw new Error(`Unknown login command: ${loginSubcommand}`);
14
+ }
15
+ const stdout = dependencies.stdout ?? process.stdout;
16
+ const savedConfig = await (dependencies.readSavedConfig ?? readSavedConfigMetadata)();
17
+ const hosts = getConfiguredLoginHosts(parsedArgs, savedConfig);
18
+ const apiKey = getLoginApiKey(parsedArgs);
19
+ if (apiKey != null) {
20
+ await saveApiKeyLogin({
21
+ apiKey,
22
+ hosts,
23
+ saveConfig: dependencies.saveConfig ?? saveConfig,
24
+ stdout,
25
+ });
26
+ return;
27
+ }
28
+ await runBrowserLogin(parsedArgs, dependencies, { hosts, stdout });
29
+ }
30
+ async function runBrowserLogin(parsedArgs, dependencies, context) {
31
+ const session = await (dependencies.createCliLoginSession ?? createCliLoginSession)({
32
+ baseUrl: context.hosts.baseUrl,
33
+ });
34
+ if (hasFlag(parsedArgs, "no-browser")) {
35
+ await (dependencies.savePendingCliLoginSession ?? savePendingCliLoginSession)({
36
+ ...session,
37
+ ...context.hosts,
38
+ });
39
+ writeNoBrowserLoginInstructions(context.stdout, session);
40
+ return;
41
+ }
42
+ context.stdout.write(`Opening browser to log in. Confirm that this verification code matches what you see in the browser:\n\n ${session.verificationCode}\n\n`);
43
+ try {
44
+ await (dependencies.openBrowser ?? openBrowser)(session.browserUrl);
45
+ }
46
+ catch {
47
+ await (dependencies.savePendingCliLoginSession ?? savePendingCliLoginSession)({
48
+ ...session,
49
+ ...context.hosts,
50
+ });
51
+ context.stdout.write("Could not open the browser automatically.\n\n");
52
+ writeNoBrowserLoginInstructions(context.stdout, session);
53
+ return;
54
+ }
55
+ context.stdout.write("Waiting for confirmation...");
56
+ const approvedSession = await (dependencies.pollCliLoginSession ?? pollCliLoginSession)({
57
+ baseUrl: context.hosts.baseUrl,
58
+ cliAuthSessionId: session.cliAuthSessionId,
59
+ expiresAt: session.expiresAt,
60
+ now: dependencies.now,
61
+ pollToken: session.pollToken,
62
+ redeemSession: dependencies.redeemCliLoginSession,
63
+ sleep: dependencies.sleep,
64
+ });
65
+ await (dependencies.saveConfig ?? saveConfig)({
66
+ apiKey: approvedSession.apiKey,
67
+ credentialSource: "browser_login",
68
+ ...context.hosts,
69
+ ...getApprovedSessionWorkspaceConfig(approvedSession),
70
+ });
71
+ context.stdout.write("\r✓ Authenticated! \n");
72
+ }
73
+ async function runLoginPoll(dependencies) {
74
+ const stdout = dependencies.stdout ?? process.stdout;
75
+ const pendingSession = await (dependencies.readPendingCliLoginSession ?? readPendingCliLoginSession)();
76
+ if (pendingSession == null) {
77
+ throw new Error("No pending CLI login session. Run `ando login --no-browser` first.");
78
+ }
79
+ stdout.write("Waiting for confirmation...");
80
+ try {
81
+ const approvedSession = await (dependencies.pollCliLoginSession ?? pollCliLoginSession)({
82
+ baseUrl: pendingSession.baseUrl,
83
+ cliAuthSessionId: pendingSession.cliAuthSessionId,
84
+ expiresAt: pendingSession.expiresAt,
85
+ now: dependencies.now,
86
+ pollToken: pendingSession.pollToken,
87
+ redeemSession: dependencies.redeemCliLoginSession,
88
+ sleep: dependencies.sleep,
89
+ });
90
+ await (dependencies.saveConfig ?? saveConfig)({
91
+ apiKey: approvedSession.apiKey,
92
+ apiHost: pendingSession.apiHost,
93
+ baseUrl: pendingSession.baseUrl,
94
+ credentialSource: "browser_login",
95
+ ...getApprovedSessionWorkspaceConfig(approvedSession),
96
+ realtimeHost: pendingSession.realtimeHost,
97
+ });
98
+ await (dependencies.clearPendingCliLoginSession ?? clearPendingCliLoginSession)();
99
+ }
100
+ catch (error) {
101
+ if (error instanceof CliLoginRequestError && error.terminal) {
102
+ await (dependencies.clearPendingCliLoginSession ?? clearPendingCliLoginSession)();
103
+ }
104
+ throw error;
105
+ }
106
+ stdout.write("\r✓ Authenticated! \n");
107
+ }
108
+ function getLoginApiKey(parsedArgs) {
109
+ const explicitApiKey = getStringFlag(parsedArgs, "api-key");
110
+ if (explicitApiKey != null) {
111
+ if (hasFlag(parsedArgs, "no-browser")) {
112
+ throw new Error("Use either --api-key or --no-browser, not both.");
113
+ }
114
+ return explicitApiKey.trim();
115
+ }
116
+ if (hasFlag(parsedArgs, "no-browser")) {
117
+ return null;
118
+ }
119
+ const apiKey = process.env["ANDO_API_KEY"];
120
+ return apiKey == null ? null : apiKey.trim();
121
+ }
122
+ function getConfiguredLoginHosts(parsedArgs, savedConfig) {
123
+ return {
124
+ baseUrl: getConfiguredBaseUrl(parsedArgs, savedConfig?.baseUrl ?? null) ??
125
+ DEFAULT_ANDO_BASE_URL,
126
+ apiHost: getConfiguredApiHost(parsedArgs, savedConfig?.apiHost ?? null),
127
+ realtimeHost: getConfiguredRealtimeHost(parsedArgs, savedConfig?.realtimeHost ?? null),
128
+ };
129
+ }
130
+ async function saveApiKeyLogin(params) {
131
+ if (params.apiKey === "") {
132
+ throw new Error("login requires a non-empty --api-key or ANDO_API_KEY.");
133
+ }
134
+ await params.saveConfig({
135
+ apiKey: params.apiKey,
136
+ credentialSource: "api_key",
137
+ ...params.hosts,
138
+ });
139
+ params.stdout.write(`Saved CLI session to ${getConfigPath()}\n`);
140
+ params.stdout.write("Configured API key authentication.\n");
141
+ }
142
+ function getApprovedSessionWorkspaceConfig(session) {
143
+ return {
144
+ ...(session.workspaceId != null ? { defaultWorkspaceId: session.workspaceId } : {}),
145
+ ...(session.workspaceMembershipId != null
146
+ ? { defaultWorkspaceMembershipId: session.workspaceMembershipId }
147
+ : {}),
148
+ };
149
+ }
150
+ function writeNoBrowserLoginInstructions(stdout, session) {
151
+ stdout.write(`Open this URL in your browser to log in:\n\n ${session.browserUrl}\n\n`);
152
+ stdout.write(`Confirm that this verification code matches what you see in the browser:\n\n ${session.verificationCode}\n\n`);
153
+ stdout.write("Then run this command to complete login:\n\n ando login poll\n\n");
154
+ stdout.write(" hint: For agents: You must show the URL above to the user to click, then run `ando login poll`. The poll command will wait for the user to confirm in the browser and exit once login is complete. Do not ask the user before polling -- they have already requested to log in.\n");
155
+ }
@@ -0,0 +1,68 @@
1
+ function isRecord(value) {
2
+ return value != null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ function getNonEmptyString(source, key) {
5
+ const value = isRecord(source) ? source[key] : null;
6
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
7
+ }
8
+ export function readCredentialSource(source) {
9
+ const credentialSource = getNonEmptyString(source, "source");
10
+ return credentialSource === "api_key" || credentialSource === "browser_login"
11
+ ? credentialSource
12
+ : undefined;
13
+ }
14
+ export function getCredentialMetadataKey(storage, account) {
15
+ return `${storage}:${account}`;
16
+ }
17
+ function readConfigCredentialSource(parsed) {
18
+ if (!isRecord(parsed) || !isRecord(parsed["credential"])) {
19
+ return undefined;
20
+ }
21
+ const account = getNonEmptyString(parsed["credential"], "account");
22
+ const storage = getNonEmptyString(parsed["credential"], "storage");
23
+ return account != null && (storage === "keyring" || storage === "file")
24
+ ? readCredentialSource(parsed["credential"])
25
+ : undefined;
26
+ }
27
+ function readCredentialMetadata(source) {
28
+ return {
29
+ apiHost: getNonEmptyString(source, "apiHost") ?? undefined,
30
+ baseUrl: getNonEmptyString(source, "baseUrl") ?? undefined,
31
+ defaultWorkspaceId: getNonEmptyString(source, "defaultWorkspaceId") ?? undefined,
32
+ defaultWorkspaceMembershipId: getNonEmptyString(source, "defaultWorkspaceMembershipId") ?? undefined,
33
+ realtimeHost: getNonEmptyString(source, "realtimeHost") ?? undefined,
34
+ source: readCredentialSource(source),
35
+ };
36
+ }
37
+ export function buildSavedConfigMetadata(parsed) {
38
+ return {
39
+ apiHost: getNonEmptyString(parsed, "apiHost") ?? undefined,
40
+ baseUrl: getNonEmptyString(parsed, "baseUrl") ?? undefined,
41
+ credentialSource: readConfigCredentialSource(parsed),
42
+ defaultWorkspaceId: getNonEmptyString(parsed, "defaultWorkspaceId") ?? undefined,
43
+ defaultWorkspaceMembershipId: getNonEmptyString(parsed, "defaultWorkspaceMembershipId") ?? undefined,
44
+ realtimeHost: getNonEmptyString(parsed, "realtimeHost") ?? undefined,
45
+ };
46
+ }
47
+ export function readCredentialMetadataMap(source) {
48
+ const metadata = new Map();
49
+ if (!isRecord(source) || !isRecord(source["credentialMetadata"])) {
50
+ return metadata;
51
+ }
52
+ for (const [account, value] of Object.entries(source["credentialMetadata"])) {
53
+ if (account.trim() !== "" && isRecord(value)) {
54
+ metadata.set(account, readCredentialMetadata(value));
55
+ }
56
+ }
57
+ return metadata;
58
+ }
59
+ export function getSaveCredentialMetadata(config) {
60
+ return {
61
+ apiHost: config.apiHost,
62
+ baseUrl: config.baseUrl,
63
+ defaultWorkspaceId: config.defaultWorkspaceId,
64
+ defaultWorkspaceMembershipId: config.defaultWorkspaceMembershipId,
65
+ realtimeHost: config.realtimeHost,
66
+ source: config.credentialSource,
67
+ };
68
+ }