Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions extensions/cli/src/__mocks__/auth/workos.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { vi } from "vitest";
import type { AuthConfig } from "../../auth/workos.js";

export const isAuthenticated = vi.fn(() => false);
export const isAuthenticated = vi.fn(() => Promise.resolve(false));
export const isAuthenticatedConfig = vi.fn(() => false);
export const isEnvironmentAuthConfig = vi.fn(() => false);
export const loadAuthConfig = vi.fn(() => null);
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/auth/ensureAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isAuthenticated, login } from "./workos.js";
export async function ensureAuthenticated(
requireAuth: boolean = true,
): Promise<boolean> {
if (isAuthenticated()) {
if (await isAuthenticated()) {
return true;
}

Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/auth/orgSelection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
autoSelectOrganizationAndConfig,
createUpdatedAuthConfig,
} from "./orgSelection.js";
import type { AuthenticatedConfig } from "./workos.js";
import { AuthenticatedConfig } from "./workos-types.js";

// Mock dependencies
vi.mock("fs");
Expand Down
4 changes: 3 additions & 1 deletion extensions/cli/src/auth/orgSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import * as path from "path";

import chalk from "chalk";

import type { AuthConfig, AuthenticatedConfig } from "../auth/workos.js";
import type { AuthConfig } from "../auth/workos.js";
import { saveAuthConfig } from "../auth/workos.js";
import { getApiClient } from "../config.js";
import { env } from "../env.js";

import { AuthenticatedConfig } from "./workos-types.js";

/**
* Creates an updated AuthenticatedConfig with a new organization ID and optional config URI
*/
Expand Down
4 changes: 2 additions & 2 deletions extensions/cli/src/auth/workos-org.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import chalk from "chalk";
import { describe, expect, test, beforeEach, vi } from "vitest";
import { beforeEach, describe, expect, test, vi } from "vitest";

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

import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
import { ensureOrganization } from "./workos.js";
import type { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos.js";

// Mock dependencies
vi.mock("../config.js", () => ({
Expand Down
38 changes: 38 additions & 0 deletions extensions/cli/src/auth/workos-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Device authorization response from WorkOS
*/
export interface DeviceAuthorizationResponse {
device_code: string;
user_code: string;
verification_uri: string;
verification_uri_complete: string;
expires_in: number;
interval: number;
}

// Represents an authenticated user's configuration
export interface AuthenticatedConfig {
userId: string;
userEmail: string;
accessToken: string;
refreshToken: string;
expiresAt: number;
organizationId: string | null | undefined; // null means personal organization, undefined triggers auto-selection
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
modelName?: string; // Name of the selected model
}

// Represents configuration when using environment variable auth
export interface EnvironmentAuthConfig {
/**
* This userId?: undefined; field a trick to help TypeScript differentiate between
* AuthenticatedConfig and EnvironmentAuthConfig. Otherwise AuthenticatedConfig is
* a possible subtype of EnvironmentAuthConfig and TypeScript gets confused where
* type guards are involved.
*/
userId?: undefined;
accessToken: string;
organizationId: string | null; // Can be set via --org flag in headless mode
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
modelName?: string; // Name of the selected model
}
7 changes: 2 additions & 5 deletions extensions/cli/src/auth/workos.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import { getApiClient } from "../config.js";
import { safeStderr } from "../init.js";
import { gracefulExit } from "../util/exit.js";

import type {
AuthConfig,
AuthenticatedConfig,
EnvironmentAuthConfig,
} from "./workos.js";
import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
import type { AuthConfig } from "./workos.js";
import { saveAuthConfig } from "./workos.js";

/**
Expand Down
3 changes: 1 addition & 2 deletions extensions/cli/src/auth/workos.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { slugToUri } from "./uriUtils.js";
import { AuthenticatedConfig, EnvironmentAuthConfig } from "./workos-types.js";
import {
AuthenticatedConfig,
EnvironmentAuthConfig,
getAccessToken,
getAssistantSlug,
getOrganizationId,
Expand Down
66 changes: 15 additions & 51 deletions extensions/cli/src/auth/workos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import * as os from "os";
import * as path from "path";

import chalk from "chalk";
// Polyfill fetch for Node < 18
import nodeFetch from "node-fetch";
import open from "open";

import { logger } from "src/util/logger.js";

import { getApiClient } from "../config.js";
// eslint-disable-next-line import/order
import { env } from "../env.js";

if (!globalThis.fetch) {
globalThis.fetch = nodeFetch as unknown as typeof globalThis.fetch;
}
Expand All @@ -21,33 +23,6 @@ function getAuthConfigPath() {
return path.join(continueHome, "auth.json");
}

// Represents an authenticated user's configuration
export interface AuthenticatedConfig {
userId: string;
userEmail: string;
accessToken: string;
refreshToken: string;
expiresAt: number;
organizationId: string | null | undefined; // null means personal organization, undefined triggers auto-selection
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
modelName?: string; // Name of the selected model
}

// Represents configuration when using environment variable auth
export interface EnvironmentAuthConfig {
/**
* This userId?: undefined; field a trick to help TypeScript differentiate between
* AuthenticatedConfig and EnvironmentAuthConfig. Otherwise AuthenticatedConfig is
* a possible subtype of EnvironmentAuthConfig and TypeScript gets confused where
* type guards are involved.
*/
userId?: undefined;
accessToken: string;
organizationId: string | null; // Can be set via --org flag in headless mode
configUri?: string; // Optional config URI (file:// or slug://owner/slug)
modelName?: string; // Name of the selected model
}

// Union type representing the possible authentication states
export type AuthConfig = AuthenticatedConfig | EnvironmentAuthConfig | null;

Expand Down Expand Up @@ -117,6 +92,11 @@ import {

import { autoSelectOrganizationAndConfig } from "./orgSelection.js";
import { pathToUri, slugToUri, uriToPath, uriToSlug } from "./uriUtils.js";
import {
AuthenticatedConfig,
DeviceAuthorizationResponse,
EnvironmentAuthConfig,
} from "./workos-types.js";
import {
handleCliOrgForAuthenticatedConfig,
handleCliOrgForEnvironmentAuth,
Expand Down Expand Up @@ -266,46 +246,30 @@ export function updateLocalConfigPath(localConfigPath: string | null): void {
/**
* Checks if the user is authenticated and the token is valid
*/
export function isAuthenticated(): boolean {
export async function isAuthenticated(): Promise<boolean> {
const config = loadAuthConfig();

if (config === null) {
return false;
}

// Environment auth is always valid
if (isEnvironmentAuthConfig(config)) {
return true;
}

/**
* THIS CODE DOESN'T WORK.
* .catch() will never return in a non-async function.
* It's a hallucination.
**/
if (Date.now() > config.expiresAt) {
// Try refreshing the token
refreshToken(config.refreshToken).catch(() => {
// If refresh fails, we're not authenticated
try {
const refreshed = await refreshToken(config.refreshToken);
return isAuthenticatedConfig(refreshed);
} catch (e) {
logger.error("Failed to refresh auto token", e);
return false;
});
}
}

return true;
}

/**
* Device authorization response from WorkOS
*/
interface DeviceAuthorizationResponse {
device_code: string;
user_code: string;
verification_uri: string;
verification_uri_complete: string;
expires_in: number;
interval: number;
}

/**
* Request device authorization from WorkOS
*/
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/infoScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export async function handleInfoSlashCommand() {
);

// Auth info
if (isAuthenticated()) {
if (await isAuthenticated()) {
const config = loadAuthConfig();
if (config && isAuthenticatedConfig(config)) {
const email = config.userEmail || config.userId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import * as path from "path";
import { AssistantUnrolled, ModelConfig } from "@continuedev/config-yaml";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

import { AuthenticatedConfig } from "src/auth/workos-types.js";

import {
AuthenticatedConfig,
getModelName,
loadAuthConfig,
saveAuthConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import * as path from "path";
import { AssistantUnrolled, ModelConfig } from "@continuedev/config-yaml";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";

import { AuthenticatedConfig } from "src/auth/workos-types.js";
import { AuthService } from "src/services/AuthService.js";

import {
AuthenticatedConfig,
getModelName,
loadAuthConfig,
saveAuthConfig,
Expand Down
3 changes: 2 additions & 1 deletion extensions/cli/src/integration/model-persistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import * as path from "path";

import { afterEach, beforeEach, describe, expect, test } from "vitest";

import { AuthenticatedConfig } from "src/auth/workos-types.js";

import {
AuthenticatedConfig,
getModelName,
loadAuthConfig,
saveAuthConfig,
Expand Down
Loading
Loading