Skip to content

Commit cbc567e

Browse files
jrutarn
authored andcommitted
Cloud: support alternate auth token from environment (RooCodeInc#5323)
1 parent f4a90a1 commit cbc567e

File tree

10 files changed

+321
-44
lines changed

10 files changed

+321
-44
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {
1010
import { TelemetryService } from "@roo-code/telemetry"
1111

1212
import { CloudServiceCallbacks } from "./types"
13-
import { AuthService } from "./AuthService"
13+
import type { AuthService } from "./auth"
14+
import { WebAuthService, StaticTokenAuthService } from "./auth"
1415
import { SettingsService } from "./SettingsService"
1516
import { TelemetryClient } from "./TelemetryClient"
1617
import { ShareService, TaskNotFoundError } from "./ShareService"
@@ -43,7 +44,13 @@ export class CloudService {
4344
}
4445

4546
try {
46-
this.authService = new AuthService(this.context, this.log)
47+
const cloudToken = process.env.ROO_CODE_CLOUD_TOKEN
48+
if (cloudToken && cloudToken.length > 0) {
49+
this.authService = new StaticTokenAuthService(this.context, cloudToken, this.log)
50+
} else {
51+
this.authService = new WebAuthService(this.context, this.log)
52+
}
53+
4754
await this.authService.initialize()
4855

4956
this.authService.on("attempting-session", this.authListener)

packages/cloud/src/SettingsService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "@roo-code/types"
99

1010
import { getRooCodeApiUrl } from "./Config"
11-
import { AuthService } from "./AuthService"
11+
import type { AuthService } from "./auth"
1212
import { RefreshTimer } from "./RefreshTimer"
1313

1414
const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"

packages/cloud/src/TelemetryClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { BaseTelemetryClient } from "@roo-code/telemetry"
88

99
import { getRooCodeApiUrl } from "./Config"
10-
import { AuthService } from "./AuthService"
10+
import type { AuthService } from "./auth"
1111
import { SettingsService } from "./SettingsService"
1212

1313
export class TelemetryClient extends BaseTelemetryClient {

packages/cloud/src/__tests__/CloudService.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as vscode from "vscode"
44
import type { ClineMessage } from "@roo-code/types"
55

66
import { CloudService } from "../CloudService"
7-
import { AuthService } from "../AuthService"
7+
import { WebAuthService } from "../auth/WebAuthService"
88
import { SettingsService } from "../SettingsService"
99
import { ShareService, TaskNotFoundError } from "../ShareService"
1010
import { TelemetryClient } from "../TelemetryClient"
@@ -27,7 +27,7 @@ vi.mock("vscode", () => ({
2727

2828
vi.mock("@roo-code/telemetry")
2929

30-
vi.mock("../AuthService")
30+
vi.mock("../auth/WebAuthService")
3131

3232
vi.mock("../SettingsService")
3333

@@ -149,7 +149,7 @@ describe("CloudService", () => {
149149
},
150150
}
151151

152-
vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService)
152+
vi.mocked(WebAuthService).mockImplementation(() => mockAuthService as unknown as WebAuthService)
153153
vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService)
154154
vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService)
155155
vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
@@ -175,7 +175,7 @@ describe("CloudService", () => {
175175
const cloudService = await CloudService.createInstance(mockContext, callbacks)
176176

177177
expect(cloudService).toBeInstanceOf(CloudService)
178-
expect(AuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
178+
expect(WebAuthService).toHaveBeenCalledWith(mockContext, expect.any(Function))
179179
expect(SettingsService).toHaveBeenCalledWith(
180180
mockContext,
181181
mockAuthService,
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { describe, it, expect, beforeEach, vi } from "vitest"
2+
import * as vscode from "vscode"
3+
4+
import { StaticTokenAuthService } from "../../auth/StaticTokenAuthService"
5+
6+
// Mock vscode
7+
vi.mock("vscode", () => ({
8+
window: {
9+
showInformationMessage: vi.fn(),
10+
},
11+
env: {
12+
openExternal: vi.fn(),
13+
uriScheme: "vscode",
14+
},
15+
Uri: {
16+
parse: vi.fn(),
17+
},
18+
}))
19+
20+
describe("StaticTokenAuthService", () => {
21+
let authService: StaticTokenAuthService
22+
let mockContext: vscode.ExtensionContext
23+
let mockLog: (...args: unknown[]) => void
24+
const testToken = "test-static-token"
25+
26+
beforeEach(() => {
27+
mockLog = vi.fn()
28+
29+
// Create a minimal mock that satisfies the constructor requirements
30+
const mockContextPartial = {
31+
extension: {
32+
packageJSON: {
33+
publisher: "TestPublisher",
34+
name: "test-extension",
35+
},
36+
},
37+
globalState: {
38+
get: vi.fn(),
39+
update: vi.fn(),
40+
},
41+
secrets: {
42+
get: vi.fn(),
43+
store: vi.fn(),
44+
delete: vi.fn(),
45+
onDidChange: vi.fn(),
46+
},
47+
subscriptions: [],
48+
}
49+
50+
// Use type assertion for test mocking
51+
mockContext = mockContextPartial as unknown as vscode.ExtensionContext
52+
53+
authService = new StaticTokenAuthService(mockContext, testToken, mockLog)
54+
})
55+
56+
afterEach(() => {
57+
vi.clearAllMocks()
58+
})
59+
60+
describe("constructor", () => {
61+
it("should create instance and log static token mode", () => {
62+
expect(authService).toBeInstanceOf(StaticTokenAuthService)
63+
expect(mockLog).toHaveBeenCalledWith("[auth] Using static token authentication mode")
64+
})
65+
66+
it("should use console.log as default logger", () => {
67+
const serviceWithoutLog = new StaticTokenAuthService(
68+
mockContext as unknown as vscode.ExtensionContext,
69+
testToken,
70+
)
71+
// Can't directly test console.log usage, but constructor should not throw
72+
expect(serviceWithoutLog).toBeInstanceOf(StaticTokenAuthService)
73+
})
74+
})
75+
76+
describe("initialize", () => {
77+
it("should start in active-session state", async () => {
78+
await authService.initialize()
79+
expect(authService.getState()).toBe("active-session")
80+
})
81+
82+
it("should emit active-session event on initialize", async () => {
83+
const spy = vi.fn()
84+
authService.on("active-session", spy)
85+
86+
await authService.initialize()
87+
88+
expect(spy).toHaveBeenCalledWith({ previousState: "initializing" })
89+
})
90+
91+
it("should log successful initialization", async () => {
92+
await authService.initialize()
93+
expect(mockLog).toHaveBeenCalledWith("[auth] Static token auth service initialized in active-session state")
94+
})
95+
})
96+
97+
describe("getSessionToken", () => {
98+
it("should return the provided token", () => {
99+
expect(authService.getSessionToken()).toBe(testToken)
100+
})
101+
102+
it("should return different token when constructed with different token", () => {
103+
const differentToken = "different-token"
104+
const differentService = new StaticTokenAuthService(mockContext, differentToken, mockLog)
105+
expect(differentService.getSessionToken()).toBe(differentToken)
106+
})
107+
})
108+
109+
describe("getUserInfo", () => {
110+
it("should return empty object", () => {
111+
expect(authService.getUserInfo()).toEqual({})
112+
})
113+
})
114+
115+
describe("getStoredOrganizationId", () => {
116+
it("should return null", () => {
117+
expect(authService.getStoredOrganizationId()).toBeNull()
118+
})
119+
})
120+
121+
describe("authentication state methods", () => {
122+
it("should always return true for isAuthenticated", () => {
123+
expect(authService.isAuthenticated()).toBe(true)
124+
})
125+
126+
it("should always return true for hasActiveSession", () => {
127+
expect(authService.hasActiveSession()).toBe(true)
128+
})
129+
130+
it("should always return true for hasOrIsAcquiringActiveSession", () => {
131+
expect(authService.hasOrIsAcquiringActiveSession()).toBe(true)
132+
})
133+
134+
it("should return active-session for getState", () => {
135+
expect(authService.getState()).toBe("active-session")
136+
})
137+
})
138+
139+
describe("disabled authentication methods", () => {
140+
const expectedErrorMessage = "Authentication methods are disabled in StaticTokenAuthService"
141+
142+
it("should throw error for login", async () => {
143+
await expect(authService.login()).rejects.toThrow(expectedErrorMessage)
144+
})
145+
146+
it("should throw error for logout", async () => {
147+
await expect(authService.logout()).rejects.toThrow(expectedErrorMessage)
148+
})
149+
150+
it("should throw error for handleCallback", async () => {
151+
await expect(authService.handleCallback("code", "state")).rejects.toThrow(expectedErrorMessage)
152+
})
153+
154+
it("should throw error for handleCallback with organization", async () => {
155+
await expect(authService.handleCallback("code", "state", "org_123")).rejects.toThrow(expectedErrorMessage)
156+
})
157+
})
158+
159+
describe("event emission", () => {
160+
it("should be able to register and emit events", async () => {
161+
const activeSessionSpy = vi.fn()
162+
const userInfoSpy = vi.fn()
163+
164+
authService.on("active-session", activeSessionSpy)
165+
authService.on("user-info", userInfoSpy)
166+
167+
await authService.initialize()
168+
169+
expect(activeSessionSpy).toHaveBeenCalledWith({ previousState: "initializing" })
170+
// user-info event is not emitted in static token mode
171+
expect(userInfoSpy).not.toHaveBeenCalled()
172+
})
173+
})
174+
})

packages/cloud/src/__tests__/AuthService.spec.ts renamed to packages/cloud/src/__tests__/auth/WebAuthService.spec.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import { vi, Mock, beforeEach, afterEach, describe, it, expect } from "vitest"
44
import crypto from "crypto"
55
import * as vscode from "vscode"
66

7-
import { AuthService } from "../AuthService"
8-
import { RefreshTimer } from "../RefreshTimer"
9-
import * as Config from "../Config"
10-
import * as utils from "../utils"
7+
import { WebAuthService } from "../../auth/WebAuthService"
8+
import { RefreshTimer } from "../../RefreshTimer"
9+
import * as Config from "../../Config"
10+
import * as utils from "../../utils"
1111

1212
// Mock external dependencies
13-
vi.mock("../RefreshTimer")
14-
vi.mock("../Config")
15-
vi.mock("../utils")
13+
vi.mock("../../RefreshTimer")
14+
vi.mock("../../Config")
15+
vi.mock("../../utils")
1616
vi.mock("crypto")
1717

1818
// Mock fetch globally
@@ -34,8 +34,8 @@ vi.mock("vscode", () => ({
3434
},
3535
}))
3636

37-
describe("AuthService", () => {
38-
let authService: AuthService
37+
describe("WebAuthService", () => {
38+
let authService: WebAuthService
3939
let mockTimer: {
4040
start: Mock
4141
stop: Mock
@@ -97,7 +97,8 @@ describe("AuthService", () => {
9797
stop: vi.fn(),
9898
reset: vi.fn(),
9999
}
100-
vi.mocked(RefreshTimer).mockImplementation(() => mockTimer as unknown as RefreshTimer)
100+
const MockedRefreshTimer = vi.mocked(RefreshTimer)
101+
MockedRefreshTimer.mockImplementation(() => mockTimer as unknown as RefreshTimer)
101102

102103
// Setup config mocks - use production URL by default to maintain existing test behavior
103104
vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
@@ -112,7 +113,7 @@ describe("AuthService", () => {
112113
// Setup log mock
113114
mockLog = vi.fn()
114115

115-
authService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
116+
authService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
116117
})
117118

118119
afterEach(() => {
@@ -138,9 +139,9 @@ describe("AuthService", () => {
138139
})
139140

140141
it("should use console.log as default logger", () => {
141-
const serviceWithoutLog = new AuthService(mockContext as unknown as vscode.ExtensionContext)
142+
const serviceWithoutLog = new WebAuthService(mockContext as unknown as vscode.ExtensionContext)
142143
// Can't directly test console.log usage, but constructor should not throw
143-
expect(serviceWithoutLog).toBeInstanceOf(AuthService)
144+
expect(serviceWithoutLog).toBeInstanceOf(WebAuthService)
144145
})
145146
})
146147

@@ -434,7 +435,7 @@ describe("AuthService", () => {
434435
const credentials = { clientToken: "test-token", sessionId: "test-session" }
435436
mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
436437

437-
const authenticatedService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
438+
const authenticatedService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
438439
await authenticatedService.initialize()
439440

440441
expect(authenticatedService.isAuthenticated()).toBe(true)
@@ -460,7 +461,7 @@ describe("AuthService", () => {
460461
const credentials = { clientToken: "test-token", sessionId: "test-session" }
461462
mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
462463

463-
const attemptingService = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
464+
const attemptingService = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
464465
await attemptingService.initialize()
465466

466467
expect(attemptingService.hasOrIsAcquiringActiveSession()).toBe(true)
@@ -960,7 +961,7 @@ describe("AuthService", () => {
960961
// Mock getClerkBaseUrl to return production URL
961962
vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
962963

963-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
964+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
964965
const credentials = { clientToken: "test-token", sessionId: "test-session" }
965966

966967
await service.initialize()
@@ -977,7 +978,7 @@ describe("AuthService", () => {
977978
// Mock getClerkBaseUrl to return custom URL
978979
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
979980

980-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
981+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
981982
const credentials = { clientToken: "test-token", sessionId: "test-session" }
982983

983984
await service.initialize()
@@ -993,7 +994,7 @@ describe("AuthService", () => {
993994
const customUrl = "https://custom.clerk.com"
994995
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
995996

996-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
997+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
997998
const credentials = { clientToken: "test-token", sessionId: "test-session" }
998999
mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
9991000

@@ -1008,7 +1009,7 @@ describe("AuthService", () => {
10081009
const customUrl = "https://custom.clerk.com"
10091010
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
10101011

1011-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
1012+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
10121013

10131014
await service.initialize()
10141015
await service["clearCredentials"]()
@@ -1027,7 +1028,7 @@ describe("AuthService", () => {
10271028
return { dispose: vi.fn() }
10281029
})
10291030

1030-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
1031+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
10311032
await service.initialize()
10321033

10331034
// Simulate credentials change event with scoped key
@@ -1054,7 +1055,7 @@ describe("AuthService", () => {
10541055
return { dispose: vi.fn() }
10551056
})
10561057

1057-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
1058+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
10581059
await service.initialize()
10591060

10601061
const inactiveSessionSpy = vi.fn()
@@ -1078,7 +1079,7 @@ describe("AuthService", () => {
10781079
return { dispose: vi.fn() }
10791080
})
10801081

1081-
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
1082+
const service = new WebAuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
10821083
await service.initialize()
10831084

10841085
const inactiveSessionSpy = vi.fn()

0 commit comments

Comments
 (0)