2
0
Эх сурвалжийг харах

Cloud: support alternate auth token from environment (#5323)

John Richmond 6 сар өмнө
parent
commit
05040414c2

+ 9 - 2
packages/cloud/src/CloudService.ts

@@ -10,7 +10,8 @@ import type {
 import { TelemetryService } from "@roo-code/telemetry"
 
 import { CloudServiceCallbacks } from "./types"
-import { AuthService } from "./AuthService"
+import type { AuthService } from "./auth"
+import { WebAuthService, StaticTokenAuthService } from "./auth"
 import { SettingsService } from "./SettingsService"
 import { TelemetryClient } from "./TelemetryClient"
 import { ShareService, TaskNotFoundError } from "./ShareService"
@@ -43,7 +44,13 @@ export class CloudService {
 		}
 
 		try {
-			this.authService = new AuthService(this.context, this.log)
+			const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN
+			if (cloudToken && cloudToken.length > 0) {
+				this.authService = new StaticTokenAuthService(this.context, cloudToken, this.log)
+			} else {
+				this.authService = new WebAuthService(this.context, this.log)
+			}
+
 			await this.authService.initialize()
 
 			this.authService.on("attempting-session", this.authListener)

+ 1 - 1
packages/cloud/src/SettingsService.ts

@@ -8,7 +8,7 @@ import {
 } from "@roo-code/types"
 
 import { getRooCodeApiUrl } from "./Config"
-import { AuthService } from "./AuthService"
+import type { AuthService } from "./auth"
 import { RefreshTimer } from "./RefreshTimer"
 
 const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"

+ 1 - 1
packages/cloud/src/ShareService.ts

@@ -2,7 +2,7 @@ import * as vscode from "vscode"
 
 import { shareResponseSchema } from "@roo-code/types"
 import { getRooCodeApiUrl } from "./Config"
-import type { AuthService } from "./AuthService"
+import type { AuthService } from "./auth"
 import type { SettingsService } from "./SettingsService"
 import { getUserAgent } from "./utils"
 

+ 1 - 1
packages/cloud/src/TelemetryClient.ts

@@ -7,7 +7,7 @@ import {
 import { BaseTelemetryClient } from "@roo-code/telemetry"
 
 import { getRooCodeApiUrl } from "./Config"
-import { AuthService } from "./AuthService"
+import type { AuthService } from "./auth"
 import { SettingsService } from "./SettingsService"
 
 export class TelemetryClient extends BaseTelemetryClient {

+ 4 - 4
packages/cloud/src/__tests__/CloudService.test.ts

@@ -4,7 +4,7 @@ import * as vscode from "vscode"
 import type { ClineMessage } from "@roo-code/types"
 
 import { CloudService } from "../CloudService"
-import { AuthService } from "../AuthService"
+import { WebAuthService } from "../auth/WebAuthService"
 import { SettingsService } from "../SettingsService"
 import { ShareService, TaskNotFoundError } from "../ShareService"
 import { TelemetryClient } from "../TelemetryClient"
@@ -27,7 +27,7 @@ vi.mock("vscode", () => ({
 
 vi.mock("@roo-code/telemetry")
 
-vi.mock("../AuthService")
+vi.mock("../auth/WebAuthService")
 
 vi.mock("../SettingsService")
 
@@ -149,7 +149,7 @@ describe("CloudService", () => {
 			},
 		}
 
-		vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService)
+		vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
 		vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService)
 		vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService)
 		vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
@@ -175,7 +175,7 @@ describe("CloudService", () => {
 			const cloudService = await CloudService.createInstance(mockContext, callbacks)
 
 			expect(cloudService).toBeInstanceOf(CloudService)
-			expect(AuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
+			expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
 			expect(SettingsService).toHaveBeenCalledWith(
 				mockContext,
 				mockAuthService,

+ 1 - 1
packages/cloud/src/__tests__/ShareService.test.ts

@@ -4,7 +4,7 @@ import type { MockedFunction } from "vitest"
 import * as vscode from "vscode"
 
 import { ShareService, TaskNotFoundError } from "../ShareService"
-import type { AuthService } from "../AuthService"
+import type { AuthService } from "../auth"
 import type { SettingsService } from "../SettingsService"
 
 // Mock fetch

+ 174 - 0
packages/cloud/src/__tests__/auth/StaticTokenAuthService.spec.ts

@@ -0,0 +1,174 @@
+import { describe, it, expect, beforeEach, vi } from "vitest"
+import * as vscode from "vscode"
+
+import { StaticTokenAuthService } from "../../auth/StaticTokenAuthService"
+
+// Mock vscode
+vi.mock("vscode", () => ({
+	window: {
+		showInformationMessage: vi.fn(),
+	},
+	env: {
+		openExternal: vi.fn(),
+		uriScheme: "vscode",
+	},
+	Uri: {
+		parse: vi.fn(),
+	},
+}))
+
+describe("StaticTokenAuthService", () => {
+	let authService: StaticTokenAuthService
+	let mockContext: vscode.ExtensionContext
+	let mockLog: (...args: unknown[]) => void
+	const testToken = "test-static-token"
+
+	beforeEach(() => {
+		mockLog = vi.fn()
+
+		// Create a minimal mock that satisfies the constructor requirements
+		const mockContextPartial = {
+			extension: {
+				packageJSON: {
+					publisher: "TestPublisher",
+					name: "test-extension",
+				},
+			},
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+			},
+			secrets: {
+				get: vi.fn(),
+				store: vi.fn(),
+				delete: vi.fn(),
+				onDidChange: vi.fn(),
+			},
+			subscriptions: [],
+		}
+
+		// Use type assertion for test mocking
+		mockContext = mockContextPartial as unknown as vscode.ExtensionContext
+
+		authService = new StaticTokenAuthService(mockContext, testToken, mockLog)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("constructor", () => {
+		it("should create instance and log static token mode", () => {
+			expect(authService).toBeInstanceOf(StaticTokenAuthService)
+			expect(mockLog).toHaveBeenCalledWith("[auth] Using static token authentication mode")
+		})
+
+		it("should use console.log as default logger", () => {
+			const serviceWithoutLog = new StaticTokenAuthService(
+				mockContext as unknown as vscode.ExtensionContext,
+				testToken,
+			)
+			// Can't directly test console.log usage, but constructor should not throw
+			expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService)
+		})
+	})
+
+	describe("initialize", () => {
+		it("should start in active-session state", async () => {
+			await authService.initialize()
+			expect(authService.getState()).toBe("active-session")
+		})
+
+		it("should emit active-session event on initialize", async () => {
+			const spy = vi.fn()
+			authService.on("active-session", spy)
+
+			await authService.initialize()
+
+			expect(spy).toHaveBeenCalledWith({ previousState: "initializing" })
+		})
+
+		it("should log successful initialization", async () => {
+			await authService.initialize()
+			expect(mockLog).toHaveBeenCalledWith("[auth] Static token auth service initialized in active-session state")
+		})
+	})
+
+	describe("getSessionToken", () => {
+		it("should return the provided token", () => {
+			expect(authService.getSessionToken()).toBe(testToken)
+		})
+
+		it("should return different token when constructed with different token", () => {
+			const differentToken = "different-token"
+			const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog)
+			expect(differentService.getSessionToken()).toBe(differentToken)
+		})
+	})
+
+	describe("getUserInfo", () => {
+		it("should return empty object", () => {
+			expect(authService.getUserInfo()).toEqual({})
+		})
+	})
+
+	describe("getStoredOrganizationId", () => {
+		it("should return null", () => {
+			expect(authService.getStoredOrganizationId()).toBeNull()
+		})
+	})
+
+	describe("authentication state methods", () => {
+		it("should always return true for isAuthenticated", () => {
+			expect(authService.isAuthenticated()).toBe(true)
+		})
+
+		it("should always return true for hasActiveSession", () => {
+			expect(authService.hasActiveSession()).toBe(true)
+		})
+
+		it("should always return true for hasOrIsAcquiringActiveSession", () => {
+			expect(authService.hasOrIsAcquiringActiveSession()).toBe(true)
+		})
+
+		it("should return active-session for getState", () => {
+			expect(authService.getState()).toBe("active-session")
+		})
+	})
+
+	describe("disabled authentication methods", () => {
+		const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService"
+
+		it("should throw error for login", async () => {
+			await expect(authService.login()).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for logout", async () => {
+			await expect(authService.logout()).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for handleCallback", async () => {
+			await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage)
+		})
+
+		it("should throw error for handleCallback with organization", async () => {
+			await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage)
+		})
+	})
+
+	describe("event emission", () => {
+		it("should be able to register and emit events", async () => {
+			const activeSessionSpy = vi.fn()
+			const userInfoSpy = vi.fn()
+
+			authService.on("active-session", activeSessionSpy)
+			authService.on("user-info", userInfoSpy)
+
+			await authService.initialize()
+
+			expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" })
+			// user-info event is not emitted in static token mode
+			expect(userInfoSpy).not.toHaveBeenCalled()
+		})
+	})
+})

+ 23 - 22
packages/cloud/src/__tests__/AuthService.spec.ts → packages/cloud/src/__tests__/auth/WebAuthService.spec.ts

@@ -4,15 +4,15 @@ import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest"
 import crypto from "crypto"
 import * as vscode from "vscode"
 
-import { AuthService } from "../AuthService"
-import { RefreshTimer } from "../RefreshTimer"
-import * as Config from "../Config"
-import * as utils from "../utils"
+import { WebAuthService } from "../../auth/WebAuthService"
+import { RefreshTimer } from "../../RefreshTimer"
+import * as Config from "../../Config"
+import * as utils from "../../utils"
 
 // Mock external dependencies
-vi.mock("../RefreshTimer")
-vi.mock("../Config")
-vi.mock("../utils")
+vi.mock("../../RefreshTimer")
+vi.mock("../../Config")
+vi.mock("../../utils")
 vi.mock("crypto")
 
 // Mock fetch globally
@@ -34,8 +34,8 @@ vi.mock("vscode", () => ({
 	},
 }))
 
-describe("AuthService", () => {
-	let authService: AuthService
+describe("WebAuthService", () => {
+	let authService: WebAuthService
 	let mockTimer: {
 		start: Mock
 		stop: Mock
@@ -97,7 +97,8 @@ describe("AuthService", () => {
 			stop: vi.fn(),
 			reset: vi.fn(),
 		}
-		vi.mocked(RefreshTimer).mockImplementation(() => mockTimer as unknown as RefreshTimer)
+		const MockedRefreshTimer = vi.mocked(RefreshTimer)
+		MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer)
 
 		// Setup config mocks - use production URL by default to maintain existing test behavior
 		vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
@@ -112,7 +113,7 @@ describe("AuthService", () => {
 		// Setup log mock
 		mockLog = vi.fn()
 
-		authService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+		authService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 	})
 
 	afterEach(() => {
@@ -138,9 +139,9 @@ describe("AuthService", () => {
 		})
 
 		it("should use console.log as default logger", () => {
-			const serviceWithoutLog = new AuthService(mockContext as unknown as vscode.ExtensionContext)
+			const serviceWithoutLog = new WebAuthService(mockContext as unknown as vscode.ExtensionContext)
 			// Can't directly test console.log usage, but constructor should not throw
-			expect(serviceWithoutLog).toBeInstanceOf(AuthService)
+			expect(serviceWithoutLog).toBeInstanceOf(WebAuthService)
 		})
 	})
 
@@ -434,7 +435,7 @@ describe("AuthService", () => {
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
 
-			const authenticatedService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const authenticatedService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			await authenticatedService.initialize()
 
 			expect(authenticatedService.isAuthenticated()).toBe(true)
@@ -460,7 +461,7 @@ describe("AuthService", () => {
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
 
-			const attemptingService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const attemptingService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			await attemptingService.initialize()
 
 			expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
@@ -960,7 +961,7 @@ describe("AuthService", () => {
 			// Mock getClerkBaseUrl to return production URL
 			vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 
 			await service.initialize()
@@ -977,7 +978,7 @@ describe("AuthService", () => {
 			// Mock getClerkBaseUrl to return custom URL
 			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 
 			await service.initialize()
@@ -993,7 +994,7 @@ describe("AuthService", () => {
 			const customUrl = "https://custom.clerk.com"
 			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			const credentials = { clientToken: "test-token", sessionId: "test-session" }
 			mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
 
@@ -1008,7 +1009,7 @@ describe("AuthService", () => {
 			const customUrl = "https://custom.clerk.com"
 			vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 
 			await service.initialize()
 			await service["clearCredentials"]()
@@ -1027,7 +1028,7 @@ describe("AuthService", () => {
 				return { dispose: vi.fn() }
 			})
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			await service.initialize()
 
 			// Simulate credentials change event with scoped key
@@ -1054,7 +1055,7 @@ describe("AuthService", () => {
 				return { dispose: vi.fn() }
 			})
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			await service.initialize()
 
 			const inactiveSessionSpy = vi.fn()
@@ -1078,7 +1079,7 @@ describe("AuthService", () => {
 				return { dispose: vi.fn() }
 			})
 
-			const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
+			const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
 			await service.initialize()
 
 			const inactiveSessionSpy = vi.fn()

+ 33 - 0
packages/cloud/src/auth/AuthService.ts

@@ -0,0 +1,33 @@
+import EventEmitter from "events"
+import type { CloudUserInfo } from "@roo-code/types"
+
+export interface AuthServiceEvents {
+	"attempting-session": [data: { previousState: AuthState }]
+	"inactive-session": [data: { previousState: AuthState }]
+	"active-session": [data: { previousState: AuthState }]
+	"logged-out": [data: { previousState: AuthState }]
+	"user-info": [data: { userInfo: CloudUserInfo }]
+}
+
+export type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session"
+
+export interface AuthService extends EventEmitter<AuthServiceEvents> {
+	// Lifecycle
+	initialize(): Promise<void>
+
+	// Authentication methods
+	login(): Promise<void>
+	logout(): Promise<void>
+	handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise<void>
+
+	// State methods
+	getState(): AuthState
+	isAuthenticated(): boolean
+	hasActiveSession(): boolean
+	hasOrIsAcquiringActiveSession(): boolean
+
+	// Token and user info
+	getSessionToken(): string | undefined
+	getUserInfo(): CloudUserInfo | null
+	getStoredOrganizationId(): string | null
+}

+ 68 - 0
packages/cloud/src/auth/StaticTokenAuthService.ts

@@ -0,0 +1,68 @@
+import EventEmitter from "events"
+import * as vscode from "vscode"
+import type { CloudUserInfo } from "@roo-code/types"
+import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
+
+export class StaticTokenAuthService extends EventEmitter<AuthServiceEvents> implements AuthService {
+	private state: AuthState = "active-session"
+	private token: string
+	private log: (...args: unknown[]) => void
+
+	constructor(context: vscode.ExtensionContext, token: string, log?: (...args: unknown[]) => void) {
+		super()
+		this.token = token
+		this.log = log || console.log
+		this.log("[auth] Using static token authentication mode")
+	}
+
+	public async initialize(): Promise<void> {
+		const previousState: AuthState = "initializing"
+		this.state = "active-session"
+		this.emit("active-session", { previousState })
+		this.log("[auth] Static token auth service initialized in active-session state")
+	}
+
+	public async login(): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public async logout(): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public async handleCallback(
+		_code: string | null,
+		_state: string | null,
+		_organizationId?: string | null,
+	): Promise<void> {
+		throw new Error("Authentication methods are disabled in StaticTokenAuthService")
+	}
+
+	public getState(): AuthState {
+		return this.state
+	}
+
+	public getSessionToken(): string | undefined {
+		return this.token
+	}
+
+	public isAuthenticated(): boolean {
+		return true
+	}
+
+	public hasActiveSession(): boolean {
+		return true
+	}
+
+	public hasOrIsAcquiringActiveSession(): boolean {
+		return true
+	}
+
+	public getUserInfo(): CloudUserInfo | null {
+		return {}
+	}
+
+	public getStoredOrganizationId(): string | null {
+		return null
+	}
+}

+ 5 - 14
packages/cloud/src/AuthService.ts → packages/cloud/src/auth/WebAuthService.ts

@@ -6,17 +6,10 @@ import { z } from "zod"
 
 import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"
 
-import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "./Config"
-import { RefreshTimer } from "./RefreshTimer"
-import { getUserAgent } from "./utils"
-
-export interface AuthServiceEvents {
-	"attempting-session": [data: { previousState: AuthState }]
-	"inactive-session": [data: { previousState: AuthState }]
-	"active-session": [data: { previousState: AuthState }]
-	"logged-out": [data: { previousState: AuthState }]
-	"user-info": [data: { userInfo: CloudUserInfo }]
-}
+import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "../Config"
+import { RefreshTimer } from "../RefreshTimer"
+import { getUserAgent } from "../utils"
+import type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
 
 const authCredentialsSchema = z.object({
 	clientToken: z.string().min(1, "Client token cannot be empty"),
@@ -28,8 +21,6 @@ type AuthCredentials = z.infer<typeof authCredentialsSchema>
 
 const AUTH_STATE_KEY = "clerk-auth-state"
 
-type AuthState = "initializing" | "logged-out" | "active-session" | "attempting-session" | "inactive-session"
-
 const clerkSignInResponseSchema = z.object({
 	response: z.object({
 		created_session_id: z.string(),
@@ -85,7 +76,7 @@ class InvalidClientTokenError extends Error {
 	}
 }
 
-export class AuthService extends EventEmitter<AuthServiceEvents> {
+export class WebAuthService extends EventEmitter<AuthServiceEvents> implements AuthService {
 	private context: vscode.ExtensionContext
 	private timer: RefreshTimer
 	private state: AuthState = "initializing"

+ 3 - 0
packages/cloud/src/auth/index.ts

@@ -0,0 +1,3 @@
+export type { AuthService, AuthServiceEvents, AuthState } from "./AuthService"
+export { WebAuthService } from "./WebAuthService"
+export { StaticTokenAuthService } from "./StaticTokenAuthService"