Skip to content

Commit abe18e8

Browse files
authored
Merge pull request #8758 from continuedev/dallin/mcp-oauth-cli
feat(cli): continue oauth for cli MCPs
2 parents 9036fb8 + 26bb215 commit abe18e8

22 files changed

+637
-215
lines changed

extensions/cli/src/__mocks__/auth/workos.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { vi } from "vitest";
2-
import type { AuthConfig } from "../../auth/workos.js";
32

4-
export const isAuthenticated = vi.fn(() => false);
3+
export const isAuthenticated = vi.fn(() => Promise.resolve(false));
54
export const isAuthenticatedConfig = vi.fn(() => false);
65
export const isEnvironmentAuthConfig = vi.fn(() => false);
76
export const loadAuthConfig = vi.fn(() => null);

extensions/cli/src/auth/ensureAuth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { isAuthenticated, login } from "./workos.js";
99
export async function ensureAuthenticated(
1010
requireAuth: boolean = true,
1111
): Promise<boolean> {
12-
if (isAuthenticated()) {
12+
if (await isAuthenticated()) {
1313
return true;
1414
}
1515

extensions/cli/src/auth/orgSelection.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
autoSelectOrganizationAndConfig,
77
createUpdatedAuthConfig,
88
} from "./orgSelection.js";
9-
import type { AuthenticatedConfig } from "./workos.js";
9+
import { AuthenticatedConfig } from "./workos-types.js";
1010

1111
// Mock dependencies
1212
vi.mock("fs");

extensions/cli/src/auth/orgSelection.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import * as path from "path";
33

44
import chalk from "chalk";
55

6-
import type { AuthConfig, AuthenticatedConfig } from "../auth/workos.js";
6+
import type { AuthConfig } from "../auth/workos.js";
77
import { saveAuthConfig } from "../auth/workos.js";
88
import { getApiClient } from "../config.js";
99
import { env } from "../env.js";
1010

11+
import { AuthenticatedConfig } from "./workos-types.js";
12+
1113
/**
1214
* Creates an updated AuthenticatedConfig with a new organization ID and optional config URI
1315
*/

extensions/cli/src/auth/workos-org.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import chalk from "chalk";
2-
import { describe, expect, test, beforeEach, vi } from "vitest";
2+
import { beforeEach, describe, expect, test, vi } from "vitest";
33

44
import { getApiClient } from "../config.js";
55

6+
import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
67
import { ensureOrganization } from "./workos.js";
7-
import type { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos.js";
88

99
// Mock dependencies
1010
vi.mock("../config.js", () => ({
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Device authorization response from WorkOS
3+
*/
4+
export interface DeviceAuthorizationResponse {
5+
device_code: string;
6+
user_code: string;
7+
verification_uri: string;
8+
verification_uri_complete: string;
9+
expires_in: number;
10+
interval: number;
11+
}
12+
13+
// Represents an authenticated user's configuration
14+
export interface AuthenticatedConfig {
15+
userId: string;
16+
userEmail: string;
17+
accessToken: string;
18+
refreshToken: string;
19+
expiresAt: number;
20+
organizationId: string | null | undefined; // null means personal organization, undefined triggers auto-selection
21+
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
22+
modelName?: string; // Name of the selected model
23+
}
24+
25+
// Represents configuration when using environment variable auth
26+
export interface EnvironmentAuthConfig {
27+
/**
28+
* This userId?: undefined; field a trick to help TypeScript differentiate between
29+
* AuthenticatedConfig and EnvironmentAuthConfig. Otherwise AuthenticatedConfig is
30+
* a possible subtype of EnvironmentAuthConfig and TypeScript gets confused where
31+
* type guards are involved.
32+
*/
33+
userId?: undefined;
34+
accessToken: string;
35+
organizationId: string | null; // Can be set via --org flag in headless mode
36+
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
37+
modelName?: string; // Name of the selected model
38+
}

extensions/cli/src/auth/workos.helpers.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import { getApiClient } from "../config.js";
44
import { safeStderr } from "../init.js";
55
import { gracefulExit } from "../util/exit.js";
66

7-
import type {
8-
AuthConfig,
9-
AuthenticatedConfig,
10-
EnvironmentAuthConfig,
11-
} from "./workos.js";
7+
import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
8+
import type { AuthConfig } from "./workos.js";
129
import { saveAuthConfig } from "./workos.js";
1310

1411
/**

extensions/cli/src/auth/workos.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { slugToUri } from "./uriUtils.js";
2+
import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
23
import {
3-
AuthenticatedConfig,
4-
EnvironmentAuthConfig,
54
getAccessToken,
65
getAssistantSlug,
76
getOrganizationId,

extensions/cli/src/auth/workos.ts

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import * as os from "os";
33
import * as path from "path";
44

55
import chalk from "chalk";
6-
// Polyfill fetch for Node < 18
76
import nodeFetch from "node-fetch";
87
import open from "open";
98

9+
import { logger } from "src/util/logger.js";
10+
1011
import { getApiClient } from "../config.js";
1112
// eslint-disable-next-line import/order
1213
import { env } from "../env.js";
14+
1315
if (!globalThis.fetch) {
1416
globalThis.fetch = nodeFetch as unknown as typeof globalThis.fetch;
1517
}
@@ -21,33 +23,6 @@ function getAuthConfigPath() {
2123
return path.join(continueHome, "auth.json");
2224
}
2325

24-
// Represents an authenticated user's configuration
25-
export interface AuthenticatedConfig {
26-
userId: string;
27-
userEmail: string;
28-
accessToken: string;
29-
refreshToken: string;
30-
expiresAt: number;
31-
organizationId: string | null | undefined; // null means personal organization, undefined triggers auto-selection
32-
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
33-
modelName?: string; // Name of the selected model
34-
}
35-
36-
// Represents configuration when using environment variable auth
37-
export interface EnvironmentAuthConfig {
38-
/**
39-
* This userId?: undefined; field a trick to help TypeScript differentiate between
40-
* AuthenticatedConfig and EnvironmentAuthConfig. Otherwise AuthenticatedConfig is
41-
* a possible subtype of EnvironmentAuthConfig and TypeScript gets confused where
42-
* type guards are involved.
43-
*/
44-
userId?: undefined;
45-
accessToken: string;
46-
organizationId: string | null; // Can be set via --org flag in headless mode
47-
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
48-
modelName?: string; // Name of the selected model
49-
}
50-
5126
// Union type representing the possible authentication states
5227
export type AuthConfig = AuthenticatedConfig | EnvironmentAuthConfig | null;
5328

@@ -117,6 +92,11 @@ import {
11792

11893
import { autoSelectOrganizationAndConfig } from "./orgSelection.js";
11994
import { pathToUri, slugToUri, uriToPath, uriToSlug } from "./uriUtils.js";
95+
import {
96+
AuthenticatedConfig,
97+
DeviceAuthorizationResponse,
98+
EnvironmentAuthConfig,
99+
} from "./workos-types.js";
120100
import {
121101
handleCliOrgForAuthenticatedConfig,
122102
handleCliOrgForEnvironmentAuth,
@@ -266,46 +246,30 @@ export function updateLocalConfigPath(localConfigPath: string | null): void {
266246
/**
267247
* Checks if the user is authenticated and the token is valid
268248
*/
269-
export function isAuthenticated(): boolean {
249+
export async function isAuthenticated(): Promise<boolean> {
270250
const config = loadAuthConfig();
271251

272252
if (config === null) {
273253
return false;
274254
}
275255

276-
// Environment auth is always valid
277256
if (isEnvironmentAuthConfig(config)) {
278257
return true;
279258
}
280259

281-
/**
282-
* THIS CODE DOESN'T WORK.
283-
* .catch() will never return in a non-async function.
284-
* It's a hallucination.
285-
**/
286260
if (Date.now() > config.expiresAt) {
287-
// Try refreshing the token
288-
refreshToken(config.refreshToken).catch(() => {
289-
// If refresh fails, we're not authenticated
261+
try {
262+
const refreshed = await refreshToken(config.refreshToken);
263+
return isAuthenticatedConfig(refreshed);
264+
} catch (e) {
265+
logger.error("Failed to refresh auto token", e);
290266
return false;
291-
});
267+
}
292268
}
293269

294270
return true;
295271
}
296272

297-
/**
298-
* Device authorization response from WorkOS
299-
*/
300-
interface DeviceAuthorizationResponse {
301-
device_code: string;
302-
user_code: string;
303-
verification_uri: string;
304-
verification_uri_complete: string;
305-
expires_in: number;
306-
interval: number;
307-
}
308-
309273
/**
310274
* Request device authorization from WorkOS
311275
*/

extensions/cli/src/infoScreen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export async function handleInfoSlashCommand() {
2626
);
2727

2828
// Auth info
29-
if (isAuthenticated()) {
29+
if (await isAuthenticated()) {
3030
const config = loadAuthConfig();
3131
if (config && isAuthenticatedConfig(config)) {
3232
const email = config.userEmail || config.userId;

0 commit comments

Comments
 (0)