Skip to content

Commit b31250d

Browse files
authored
Scope dev RCC credentials by dev url (#4922)
1 parent 68e38e8 commit b31250d

File tree

2 files changed

+154
-9
lines changed

2 files changed

+154
-9
lines changed

packages/cloud/src/AuthService.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { z } from "zod"
66

77
import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"
88

9-
import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
9+
import { getClerkBaseUrl, getRooCodeApiUrl, PRODUCTION_CLERK_BASE_URL } from "./Config"
1010
import { RefreshTimer } from "./RefreshTimer"
1111
import { getUserAgent } from "./utils"
1212

@@ -24,7 +24,6 @@ const authCredentialsSchema = z.object({
2424

2525
type AuthCredentials = z.infer<typeof authCredentialsSchema>
2626

27-
const AUTH_CREDENTIALS_KEY = "clerk-auth-credentials"
2827
const AUTH_STATE_KEY = "clerk-auth-state"
2928

3029
type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
@@ -89,6 +88,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
8988
private timer: RefreshTimer
9089
private state: AuthState = "initializing"
9190
private log: (...args: unknown[]) => void
91+
private readonly authCredentialsKey: string
9292

9393
private credentials: AuthCredentials | null = null
9494
private sessionToken: string | null = null
@@ -100,6 +100,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
100100
this.context = context
101101
this.log = log || console.log
102102

103+
// Calculate auth credentials key based on Clerk base URL
104+
const clerkBaseUrl = getClerkBaseUrl()
105+
if (clerkBaseUrl !== PRODUCTION_CLERK_BASE_URL) {
106+
this.authCredentialsKey = `clerk-auth-credentials-${clerkBaseUrl}`
107+
} else {
108+
this.authCredentialsKey = "clerk-auth-credentials"
109+
}
110+
103111
this.timer = new RefreshTimer({
104112
callback: async () => {
105113
await this.refreshSession()
@@ -180,19 +188,19 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
180188

181189
this.context.subscriptions.push(
182190
this.context.secrets.onDidChange((e) => {
183-
if (e.key === AUTH_CREDENTIALS_KEY) {
191+
if (e.key === this.authCredentialsKey) {
184192
this.handleCredentialsChange()
185193
}
186194
}),
187195
)
188196
}
189197

190198
private async storeCredentials(credentials: AuthCredentials): Promise<void> {
191-
await this.context.secrets.store(AUTH_CREDENTIALS_KEY, JSON.stringify(credentials))
199+
await this.context.secrets.store(this.authCredentialsKey, JSON.stringify(credentials))
192200
}
193201

194202
private async loadCredentials(): Promise<AuthCredentials | null> {
195-
const credentialsJson = await this.context.secrets.get(AUTH_CREDENTIALS_KEY)
203+
const credentialsJson = await this.context.secrets.get(this.authCredentialsKey)
196204
if (!credentialsJson) return null
197205

198206
try {
@@ -209,7 +217,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
209217
}
210218

211219
private async clearCredentials(): Promise<void> {
212-
await this.context.secrets.delete(AUTH_CREDENTIALS_KEY)
220+
await this.context.secrets.delete(this.authCredentialsKey)
213221
}
214222

215223
/**

packages/cloud/src/__tests__/AuthService.spec.ts

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ describe("AuthService", () => {
9999
}
100100
vi.mocked(RefreshTimer).mockImplementation(() => mockTimer as unknown as RefreshTimer)
101101

102-
// Setup config mocks
103-
vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.test.com")
102+
// Setup config mocks - use production URL by default to maintain existing test behavior
103+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
104104
vi.mocked(Config.getRooCodeApiUrl).mockReturnValue("https://api.test.com")
105105

106106
// Setup utils mock
@@ -377,7 +377,7 @@ describe("AuthService", () => {
377377
expect(mockContext.secrets.delete).toHaveBeenCalledWith("clerk-auth-credentials")
378378
expect(mockContext.globalState.update).toHaveBeenCalledWith("clerk-auth-state", undefined)
379379
expect(mockFetch).toHaveBeenCalledWith(
380-
"https://clerk.test.com/v1/client/sessions/test-session/remove",
380+
"https://clerk.roocode.com/v1/client/sessions/test-session/remove",
381381
expect.objectContaining({
382382
method: "POST",
383383
headers: expect.objectContaining({
@@ -812,4 +812,141 @@ describe("AuthService", () => {
812812
expect(mockTimer.start).toHaveBeenCalled()
813813
})
814814
})
815+
816+
describe("auth credentials key scoping", () => {
817+
it("should use default key when getClerkBaseUrl returns production URL", async () => {
818+
// Mock getClerkBaseUrl to return production URL
819+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue("https://clerk.roocode.com")
820+
821+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
822+
const credentials = { clientToken: "test-token", sessionId: "test-session" }
823+
824+
await service.initialize()
825+
await service["storeCredentials"](credentials)
826+
827+
expect(mockContext.secrets.store).toHaveBeenCalledWith(
828+
"clerk-auth-credentials",
829+
JSON.stringify(credentials),
830+
)
831+
})
832+
833+
it("should use scoped key when getClerkBaseUrl returns custom URL", async () => {
834+
const customUrl = "https://custom.clerk.com"
835+
// Mock getClerkBaseUrl to return custom URL
836+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
837+
838+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
839+
const credentials = { clientToken: "test-token", sessionId: "test-session" }
840+
841+
await service.initialize()
842+
await service["storeCredentials"](credentials)
843+
844+
expect(mockContext.secrets.store).toHaveBeenCalledWith(
845+
`clerk-auth-credentials-${customUrl}`,
846+
JSON.stringify(credentials),
847+
)
848+
})
849+
850+
it("should load credentials using scoped key", async () => {
851+
const customUrl = "https://custom.clerk.com"
852+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
853+
854+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
855+
const credentials = { clientToken: "test-token", sessionId: "test-session" }
856+
mockContext.secrets.get.mockResolvedValue(JSON.stringify(credentials))
857+
858+
await service.initialize()
859+
const loadedCredentials = await service["loadCredentials"]()
860+
861+
expect(mockContext.secrets.get).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
862+
expect(loadedCredentials).toEqual(credentials)
863+
})
864+
865+
it("should clear credentials using scoped key", async () => {
866+
const customUrl = "https://custom.clerk.com"
867+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
868+
869+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
870+
871+
await service.initialize()
872+
await service["clearCredentials"]()
873+
874+
expect(mockContext.secrets.delete).toHaveBeenCalledWith(`clerk-auth-credentials-${customUrl}`)
875+
})
876+
877+
it("should listen for changes on scoped key", async () => {
878+
const customUrl = "https://custom.clerk.com"
879+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
880+
881+
let onDidChangeCallback: (e: { key: string }) => void
882+
883+
mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
884+
onDidChangeCallback = callback
885+
return { dispose: vi.fn() }
886+
})
887+
888+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
889+
await service.initialize()
890+
891+
// Simulate credentials change event with scoped key
892+
const newCredentials = { clientToken: "new-token", sessionId: "new-session" }
893+
mockContext.secrets.get.mockResolvedValue(JSON.stringify(newCredentials))
894+
895+
const inactiveSessionSpy = vi.fn()
896+
service.on("inactive-session", inactiveSessionSpy)
897+
898+
onDidChangeCallback!({ key: `clerk-auth-credentials-${customUrl}` })
899+
await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
900+
901+
expect(inactiveSessionSpy).toHaveBeenCalled()
902+
})
903+
904+
it("should not respond to changes on different scoped keys", async () => {
905+
const customUrl = "https://custom.clerk.com"
906+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
907+
908+
let onDidChangeCallback: (e: { key: string }) => void
909+
910+
mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
911+
onDidChangeCallback = callback
912+
return { dispose: vi.fn() }
913+
})
914+
915+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
916+
await service.initialize()
917+
918+
const inactiveSessionSpy = vi.fn()
919+
service.on("inactive-session", inactiveSessionSpy)
920+
921+
// Simulate credentials change event with different scoped key
922+
onDidChangeCallback!({ key: "clerk-auth-credentials-https://other.clerk.com" })
923+
await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
924+
925+
expect(inactiveSessionSpy).not.toHaveBeenCalled()
926+
})
927+
928+
it("should not respond to changes on default key when using scoped key", async () => {
929+
const customUrl = "https://custom.clerk.com"
930+
vi.mocked(Config.getClerkBaseUrl).mockReturnValue(customUrl)
931+
932+
let onDidChangeCallback: (e: { key: string }) => void
933+
934+
mockContext.secrets.onDidChange.mockImplementation((callback: (e: { key: string }) => void) => {
935+
onDidChangeCallback = callback
936+
return { dispose: vi.fn() }
937+
})
938+
939+
const service = new AuthService(mockContext as unknown as vscode.ExtensionContext, mockLog)
940+
await service.initialize()
941+
942+
const inactiveSessionSpy = vi.fn()
943+
service.on("inactive-session", inactiveSessionSpy)
944+
945+
// Simulate credentials change event with default key
946+
onDidChangeCallback!({ key: "clerk-auth-credentials" })
947+
await new Promise((resolve) => setTimeout(resolve, 0)) // Wait for async handling
948+
949+
expect(inactiveSessionSpy).not.toHaveBeenCalled()
950+
})
951+
})
815952
})

0 commit comments

Comments
 (0)