Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3160,6 +3160,8 @@ export class Task extends EventEmitter<TaskEvents> 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 }) => {
Expand Down
10 changes: 6 additions & 4 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down
44 changes: 44 additions & 0 deletions src/services/__tests__/zoo-code-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => ({
Expand Down Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions src/services/zoo-code-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,23 @@ export async function clearZooCodeToken(): Promise<void> {
}

export function getZooCodeBaseUrl(): string {
const config = vscode.workspace.getConfiguration("zoo-code")
return config.get<string>("baseUrl") || process.env.ZOO_CODE_BASE_URL || "https://www.zoocode.dev"
}

export async function startZooCodeAuth(): Promise<void> {
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"
}

Expand Down Expand Up @@ -239,6 +256,12 @@ export async function handleAuthCallback(token: string): Promise<boolean> {
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
Expand All @@ -263,13 +286,28 @@ 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<boolean> {
try {
const response = await fetch(`${baseUrl}/api/extension/auth/verify`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(10_000),
})

if (!response.ok) {
return false
}

const data = (await response.json()) as { valid?: boolean }
return data.valid === true
return "invalid"
}

Expand Down
Loading