import { mkdirSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; import { createRequire } from "node:module"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { discover, discoverSession, fetchUsage } from "./usage.js"; import type { UsageReport } from "@getpaseo/plugin/server/usage"; // node:sqlite has no @types/node@20 typings; require it with a narrow local type. const testRequire = createRequire(import.meta.url); interface TestSqliteDb { exec(sql: string): void; prepare(sql: string): { run(...params: unknown[]): void }; close(): void; } // Cursor builds have stored ItemTable values as both TEXT and BLOB. Keys map to the // real layouts: a plain modern token or the legacy JSON object. function writeCursorStateDb(homeDir: string, rows: Record): void { const dir = join(homeDir, ".config", "Cursor", "User", "globalStorage"); mkdirSync(dir, { recursive: true }); const { DatabaseSync } = testRequire("node:sqlite") as { DatabaseSync: new (path: string) => TestSqliteDb; }; const db = new DatabaseSync(join(dir, "state.vscdb")); db.exec("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); const insert = db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)"); for (const [key, value] of Object.entries(rows)) { insert.run(key, value); } db.close(); } function writeCursorAuthJson(homeDir: string, accessToken: string): void { const dir = join(homeDir, ".config", "cursor"); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, "auth.json"), JSON.stringify({ accessToken })); } function mockFetch(handlers: Map Response>): typeof fetch { return vi.fn(async (url: RequestInfo | URL) => { const key = url.toString(); const handler = handlers.get(key); if (!handler) throw new Error(`Unmocked fetch: ${key}`); return handler(); }) as unknown as typeof fetch; } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" }, }); } describe("cursor usage source", () => { let homeDir: string; let fetchApi: typeof fetch; let originalEnv: Record; beforeEach(() => { homeDir = mkdtempSync(join(tmpdir(), "usage-home-")); originalEnv = { ...process.env }; process.env["HOME"] = homeDir; process.env["USERPROFILE"] = homeDir; for (const key of [ "APPDATA", "COPILOT_TOKEN", "GITHUB_TOKEN", "GITHUB_PAT", "CURSOR_ACCESS_TOKEN", "CURSOR_TOKEN", "ZAI_API_KEY", "GLM_API_KEY", "GROK_API_KEY", "GROK_TOKEN", "KIMI_TOKEN", "KIMI_API_KEY", "KIMI_CODE_HOME", "MINIMAX_API_KEY", "MINIMAX_BASE_URL", ]) delete process.env[key]; fetchApi = mockFetch(new Map()); }); afterEach(() => { rmSync(homeDir, { recursive: true, force: true }); for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key]; for (const key in originalEnv) process.env[key] = originalEnv[key]; }); function service( _options: { platform?: typeof process.platform; keychain?: () => Promise; cursorHomeDir?: string; kimiHomeDir?: string; } = {}, ) { return { listUsage: async () => { const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "cursor", ...report, error: report.status === "error" ? report.error : null, planLabel: report.status === "available" ? (report.planLabel ?? null) : null, }, ], }; }, }; } function findProvider( result: { providers: Array<{ providerId: string } & UsageReport> }, id: string, ) { const report = result.providers.find((item) => item.providerId === id); if (!report) throw new Error(`Missing usage source ${id}`); return report; } it("fetches Cursor usage and normalizes malformed billing dates to null", async () => { process.env["CURSOR_ACCESS_TOKEN"] = "cursor_test_token"; fetchApi = mockFetch( new Map([ [ "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage", () => jsonResponse({ planUsage: { totalSpend: "1500", includedSpend: "1000", bonusSpend: "500", remaining: "2500", limit: "4000", }, billingCycleStart: "2026-01-14T12:42:14.000Z", billingCycleEnd: "not-a-date", }), ], ]), ); const cursor = findProvider(await service().listUsage(), "cursor"); expect(cursor).toMatchObject({ status: "available", balances: [ expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40, resetsAt: null, }), ], }); }); it("reads the Cursor token from the modern cursorAuth/accessToken key in state.vscdb", async () => { writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "cursor_state_jwt" }); let authorization: string | null = null; fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => { authorization = (init?.headers as Record | undefined)?.Authorization ?? null; return jsonResponse({ planUsage: { totalSpend: "1500", includedSpend: "1000", bonusSpend: "500", remaining: "2500", limit: "4000", }, billingCycleStart: "2026-01-14T12:42:14.000Z", billingCycleEnd: "2026-02-14T12:42:14.000Z", }); }) as unknown as typeof fetch; const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); expect(authorization).toBe("Bearer cursor_state_jwt"); expect(cursor).toMatchObject({ status: "available", balances: [expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40 })], }); }); it("falls back to the legacy cursorAuthStatus JSON blob when the modern key is absent", async () => { writeCursorStateDb(homeDir, { cursorAuthStatus: Buffer.from(JSON.stringify({ accessToken: "cursor_legacy_jwt" }), "utf8"), }); let authorization: string | null = null; fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => { authorization = (init?.headers as Record | undefined)?.Authorization ?? null; return jsonResponse({ planUsage: { totalSpend: "0", remaining: "100", limit: "100" }, billingCycleStart: null, billingCycleEnd: null, }); }) as unknown as typeof fetch; const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); expect(authorization).toBe("Bearer cursor_legacy_jwt"); expect(cursor.status).toBe("available"); }); it("reads the Cursor token from cursor-agent ~/.config/cursor/auth.json when desktop state is absent", async () => { writeCursorAuthJson(homeDir, "cursor_cli_jwt"); let authorization: string | null = null; fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => { authorization = (init?.headers as Record | undefined)?.Authorization ?? null; return jsonResponse({ planUsage: { totalSpend: "1500", includedSpend: "1000", bonusSpend: "500", remaining: "2500", limit: "4000", }, billingCycleStart: "2026-01-14T12:42:14.000Z", billingCycleEnd: "2026-02-14T12:42:14.000Z", }); }) as unknown as typeof fetch; const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); expect(authorization).toBe("Bearer cursor_cli_jwt"); expect(cursor).toMatchObject({ status: "available", balances: [expect.objectContaining({ id: "plan_usage", used: 15, remaining: 25, limit: 40 })], }); }); it("prefers the desktop state.vscdb token over cursor-agent auth.json", async () => { writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "cursor_desktop_jwt" }); writeCursorAuthJson(homeDir, "cursor_cli_jwt"); let authorization: string | null = null; fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => { authorization = (init?.headers as Record | undefined)?.Authorization ?? null; return jsonResponse({ planUsage: { totalSpend: "0", remaining: "100", limit: "100" }, billingCycleStart: null, billingCycleEnd: null, }); }) as unknown as typeof fetch; const cursor = findProvider(await service({ cursorHomeDir: homeDir }).listUsage(), "cursor"); expect(authorization).toBe("Bearer cursor_desktop_jwt"); expect(cursor.status).toBe("available"); }); it("omits unreadable state.vscdb", async () => { const directory = join(homeDir, ".config", "Cursor", "User", "globalStorage"); mkdirSync(directory, { recursive: true }); writeFileSync(join(directory, "state.vscdb"), "invalid database"); expect(await discover()).toEqual([]); }); describe("session discovery", () => { let sessionHome: string; beforeEach(() => { sessionHome = mkdtempSync(join(tmpdir(), "usage-session-home-")); }); afterEach(() => { rmSync(sessionHome, { recursive: true, force: true }); }); function cursorSession(env: Record, provider = "cursor") { return { kind: "session" as const, provider, env: { HOME: sessionHome, ...env } }; } function writeAuthJson(path: string): void { mkdirSync(join(path, ".."), { recursive: true }); writeFileSync(path, JSON.stringify({ accessToken: "session-token" })); } it("keeps different accounts separate while joining global and session logins for the same account", async () => { function token(sub: string, nonce: string) { return `e30.${Buffer.from(JSON.stringify({ sub, nonce })).toString("base64url")}.signature`; } writeCursorAuthJson(homeDir, token("user-a", "desktop")); writeCursorAuthJson(sessionHome, token("user-b", "session")); const globalAccounts = await discover(); const differentAccount = await discoverSession(cursorSession({}), "linux"); expect(globalAccounts).toHaveLength(1); expect(differentAccount).toHaveLength(1); expect(differentAccount[0]!.key).not.toBe(globalAccounts[0]!.key); writeCursorAuthJson(sessionHome, token("user-a", "refreshed-session")); const sameAccount = await discoverSession(cursorSession({}), "linux"); expect(sameAccount).toHaveLength(1); expect(sameAccount[0]!.key).toBe(globalAccounts[0]!.key); }); it("isolates logins without a readable subject by store and keeps their key across token rotation", async () => { writeCursorAuthJson(homeDir, "opaque-global-token"); writeCursorAuthJson(sessionHome, "opaque-session-token"); const globalAccounts = await discover(); const sessionAccounts = await discoverSession(cursorSession({}), "linux"); expect(sessionAccounts).toHaveLength(1); expect(globalAccounts).toHaveLength(1); expect(sessionAccounts[0]!.key).not.toBe(globalAccounts[0]!.key); writeCursorAuthJson(sessionHome, "e30.invalid-json.signature"); expect(await discoverSession(cursorSession({}), "linux")).toEqual(sessionAccounts); writeCursorAuthJson(sessionHome, "e30.e30.signature"); expect(await discoverSession(cursorSession({}), "linux")).toEqual(sessionAccounts); }); it("reads the login cursor-agent uses under the session's XDG_CONFIG_HOME", async () => { const configHome = join(sessionHome, "xdg"); const authPath = join(configHome, "cursor", "auth.json"); writeAuthJson(authPath); expect( await discoverSession(cursorSession({ XDG_CONFIG_HOME: configHome }), "linux"), ).toEqual([ { key: expect.stringMatching(/^store-[a-f0-9]{64}$/), input: { store: "file", locator: authPath }, }, ]); }); it("reads the login under the session's HOME, not the daemon's", async () => { writeCursorAuthJson(homeDir, "daemon-token"); writeCursorAuthJson(sessionHome, "session-token"); expect(await discoverSession(cursorSession({}), "linux")).toEqual([ { key: expect.stringMatching(/^store-[a-f0-9]{64}$/), input: { store: "file", locator: join(sessionHome, ".config", "cursor", "auth.json") }, }, ]); }); it("reads %APPDATA%/Cursor/auth.json on Windows", async () => { const appData = join(sessionHome, "AppData", "Roaming"); const authPath = join(appData, "Cursor", "auth.json"); writeAuthJson(authPath); expect(await discoverSession(cursorSession({ APPDATA: appData }), "win32")).toEqual([ { key: expect.stringMatching(/^store-[a-f0-9]{64}$/), input: { store: "file", locator: authPath }, }, ]); }); it("reads ~/.cursor/auth.json on macOS only when cursor-agent uses the file store", async () => { const authPath = join(sessionHome, ".cursor", "auth.json"); writeAuthJson(authPath); expect(await discoverSession(cursorSession({}), "darwin")).toEqual([]); expect( await discoverSession(cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "file" }), "darwin"), ).toEqual([ { key: expect.stringMatching(/^store-[a-f0-9]{64}$/), input: { store: "file", locator: authPath }, }, ]); }); it.each([ ["another provider", cursorSession({}, "claude")], ["a CURSOR_API_KEY session", cursorSession({ CURSOR_API_KEY: "key" })], ["a CURSOR_AUTH_TOKEN session", cursorSession({ CURSOR_AUTH_TOKEN: "token" })], ["an in-memory credential store", cursorSession({ AGENT_CLI_CREDENTIAL_STORE: "memory" })], ])("returns no login for %s", async (_scenario, scope) => { writeCursorAuthJson(sessionHome, "session-token"); expect(await discoverSession(scope, "linux")).toEqual([]); }); it("never falls back to the daemon's default stores", async () => { writeCursorStateDb(homeDir, { "cursorAuth/accessToken": "desktop-token" }); writeCursorAuthJson(homeDir, "daemon-token"); process.env["CURSOR_ACCESS_TOKEN"] = "daemon-env-token"; expect(await discoverSession(cursorSession({}), "linux")).toEqual([]); }); }); }); it("discovery returns a locator when fetch finds cursor credentials", async () => { const previous = process.env["CURSOR_ACCESS_TOKEN"]; try { process.env["CURSOR_ACCESS_TOKEN"] = "fixture-token"; let requested = false; await fetchFirst(async () => { requested = true; return new Response(null, { status: 401 }); }); expect(requested).toBe(true); expect(await discover()).toEqual([ { key: expect.stringMatching(/^store-[a-f0-9]{64}$/), input: { store: "env", locator: "CURSOR_ACCESS_TOKEN" }, }, ]); } finally { if (previous === undefined) delete process.env["CURSOR_ACCESS_TOKEN"]; else process.env["CURSOR_ACCESS_TOKEN"] = previous; } }); describe("account discovery", () => { it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => { const { mkdtemp, writeFile, rm } = await import("node:fs/promises"); const directory = await mkdtemp(join(tmpdir(), "usage-empty-")); const original = { ...process.env }; try { for (const key of Object.keys(process.env)) delete process.env[key]; process.env.HOME = directory; process.env.USERPROFILE = directory; if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}"); expect(await discover()).toEqual([]); } finally { for (const key of Object.keys(process.env)) delete process.env[key]; Object.assign(process.env, original); await rm(directory, { recursive: true, force: true }); } }); }); async function fetchFirst(fetchApi: typeof fetch) { const accounts = await discover(); const account = accounts[0]; if (!account) throw new Error("No configured account"); return fetchUsage(account.input as Parameters[0], fetchApi); } it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { const previous = process.env["CURSOR_ACCESS_TOKEN"]; try { process.env["CURSOR_ACCESS_TOKEN"] = "fixture-rejected-login"; const report = await fetchUsage( { store: "env", locator: "CURSOR_ACCESS_TOKEN" }, async () => new Response(null, { status }), ); expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["CURSOR_ACCESS_TOKEN"]; else process.env["CURSOR_ACCESS_TOKEN"] = previous; } });