diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 26b7295729..7f6fca6bdc 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3160,6 +3160,8 @@ export class Task extends EventEmitter implements TaskLike { cost: tokens.total ?? costResult.totalCost, }) + const mode = await this.getTaskMode().catch(() => defaultModeSlug) + // Zoo Code observability telemetry import("../../services/zoo-telemetry") .then(async ({ sendLlmTelemetry }) => { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ed26a101ef..e13bf490e5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2182,19 +2182,21 @@ export class ClineProvider deviceName: os.hostname(), } + const userInfo = getCachedZooCodeUserInfo() try { const { isZooCodeAuthenticated, getCachedZooCodeUserInfo, getZooCodeBaseUrl } = await import( "../../services/zoo-code-auth" ) - const userInfo = getCachedZooCodeUserInfo() zooCodeState = { zooCodeIsAuthenticated: await isZooCodeAuthenticated(), - zooCodeUserName: userInfo.name, - zooCodeUserEmail: userInfo.email, - zooCodeUserImage: userInfo.image, + zooCodeUserName: userInfo?.name, + zooCodeUserEmail: userInfo?.email, + zooCodeUserImage: userInfo?.image, zooCodeBaseUrl: getZooCodeBaseUrl(), deviceName: os.hostname(), } + } catch { + // Keep unauthenticated defaults if the optional auth service is unavailable. } catch { // Keep the default unauthenticated state if the optional Zoo Code auth service is unavailable. } diff --git a/src/services/__tests__/zoo-code-auth.test.ts b/src/services/__tests__/zoo-code-auth.test.ts index a182b0a34c..12e91fc7ba 100644 --- a/src/services/__tests__/zoo-code-auth.test.ts +++ b/src/services/__tests__/zoo-code-auth.test.ts @@ -27,6 +27,13 @@ vi.mock("vscode", () => ({ showErrorMessage: vi.fn(), showInformationMessage: vi.fn(), }, + env: { + asExternalUri: vi.fn(async (uri: any) => uri), + openExternal: vi.fn(), + }, + Uri: { + parse: vi.fn((value: string) => ({ toString: () => value })), + }, })) vi.mock("../i18n", () => ({ @@ -259,6 +266,43 @@ describe("zoo-code-auth", () => { }) }) + describe("handleAuthCallback", () => { + it("does not persist invalid prefixed tokens", async () => { + await initZooCodeAuth(mockContext) + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ valid: false }), + }) + + const success = await handleAuthCallback("zoo_ext_fake_token") + + expect(success).toBe(false) + expect(getCachedZooCodeToken()).toBe("") + expect(mockSecrets.store).not.toHaveBeenCalledWith("zoo-code-session-token", "zoo_ext_fake_token") + }) + + it("persists token only after backend verification succeeds", async () => { + await initZooCodeAuth(mockContext) + + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ valid: true }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ isSubscriber: true }), + }) + + const success = await handleAuthCallback("zoo_ext_real_token") + + expect(success).toBe(true) + expect(getCachedZooCodeToken()).toBe("zoo_ext_real_token") + expect(mockSecrets.store).toHaveBeenCalledWith("zoo-code-session-token", "zoo_ext_real_token") + }) + }) + describe("clearZooCodeToken", () => { it("resets the cached subscription status when the token is cleared", async () => { await initZooCodeAuth(mockContext) diff --git a/src/services/zoo-code-auth.ts b/src/services/zoo-code-auth.ts index 79fb6610ad..f7109bbb13 100644 --- a/src/services/zoo-code-auth.ts +++ b/src/services/zoo-code-auth.ts @@ -209,6 +209,23 @@ export async function clearZooCodeToken(): Promise { } export function getZooCodeBaseUrl(): string { + const config = vscode.workspace.getConfiguration("zoo-code") + return config.get("baseUrl") || process.env.ZOO_CODE_BASE_URL || "https://www.zoocode.dev" +} + +export async function startZooCodeAuth(): Promise { + const baseUrl = getZooCodeBaseUrl() + const deviceName = os.hostname() + const editor = "VS Code" + const version = Package.version + + const callbackUri = await vscode.env.asExternalUri( + vscode.Uri.parse(`vscode://${Package.publisher}.${Package.name}/auth-callback`), + ) + + const authUrl = `${baseUrl}/dashboard/connect?device=${encodeURIComponent(deviceName)}&editor=${encodeURIComponent(editor)}&version=${encodeURIComponent(version)}&callback_uri=${encodeURIComponent(callbackUri.toString())}` + + await vscode.env.openExternal(vscode.Uri.parse(authUrl)) return process.env.ZOO_CODE_BASE_URL || "https://www.zoocode.dev" } @@ -239,6 +256,12 @@ export async function handleAuthCallback(token: string): Promise { return false } + const isValid = await verifyZooCodeTokenValue(token) + if (!isValid) { + vscode.window.showErrorMessage("Zoo Code: Authentication failed. Please try signing in again.") + return false + } + await setZooCodeToken(token) // Check subscription status after successful auth @@ -263,6 +286,16 @@ export async function verifyZooCodeToken(): Promise<"valid" | "invalid" | "unrea const baseUrl = getZooCodeBaseUrl() + const isValid = await verifyZooCodeTokenValue(token, baseUrl) + if (!isValid) { + await clearZooCodeToken() + return false + } + + return true +} + +async function verifyZooCodeTokenValue(token: string, baseUrl = getZooCodeBaseUrl()): Promise { try { const response = await fetch(`${baseUrl}/api/extension/auth/verify`, { headers: { Authorization: `Bearer ${token}` }, @@ -270,6 +303,11 @@ export async function verifyZooCodeToken(): Promise<"valid" | "invalid" | "unrea }) if (!response.ok) { + return false + } + + const data = (await response.json()) as { valid?: boolean } + return data.valid === true return "invalid" }