mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-01 14:12:51 +00:00
518 lines
20 KiB
TypeScript
518 lines
20 KiB
TypeScript
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<string, string | Uint8Array>): 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<string, () => 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<string, string | undefined>;
|
|
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<unknown | null>;
|
|
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<string, string> | 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<string, string> | 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<string, string> | 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<string, string> | 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");
|
|
});
|
|
|
|
describe.each([
|
|
["modern state.vscdb", "v1"],
|
|
["modern state.vscdb", "v99"],
|
|
["legacy cursorAuthStatus", "v1"],
|
|
["auth.json", "v1"],
|
|
["CURSOR_ACCESS_TOKEN", "v1"],
|
|
["CURSOR_TOKEN", "v1"],
|
|
])("encrypted %s (%s)", (store, version) => {
|
|
beforeEach(() => {
|
|
const encrypted = `cursor-safe-storage:${version}:${Buffer.from("ciphertext").toString("base64")}`;
|
|
if (store === "modern state.vscdb") {
|
|
writeCursorStateDb(homeDir, { "cursorAuth/accessToken": encrypted });
|
|
} else if (store === "legacy cursorAuthStatus") {
|
|
writeCursorStateDb(homeDir, {
|
|
cursorAuthStatus: Buffer.from(JSON.stringify({ accessToken: encrypted }), "utf8"),
|
|
});
|
|
} else if (store === "auth.json") {
|
|
writeCursorAuthJson(homeDir, encrypted);
|
|
} else {
|
|
process.env[store] = encrypted;
|
|
}
|
|
});
|
|
|
|
it("discovers no account when the only credential is encrypted", async () => {
|
|
expect(await discover()).toEqual([]);
|
|
});
|
|
|
|
it("never sends an encrypted bearer token when fetching from a stored locator", async () => {
|
|
let input: Parameters<typeof fetchUsage>[0];
|
|
if (store === "modern state.vscdb" || store === "legacy cursorAuthStatus") {
|
|
input = {
|
|
store: "sqlite",
|
|
locator: join(homeDir, ".config", "Cursor", "User", "globalStorage", "state.vscdb"),
|
|
};
|
|
} else if (store === "auth.json") {
|
|
input = { store: "file", locator: join(homeDir, ".config", "cursor", "auth.json") };
|
|
} else {
|
|
input = { store: "env", locator: store };
|
|
}
|
|
const requests: RequestInit[] = [];
|
|
fetchApi = async (_url, init) => {
|
|
requests.push(init ?? {});
|
|
return new Response(null, { status: 401 });
|
|
};
|
|
|
|
await expect(fetchUsage(input, fetchApi)).rejects.toThrow(
|
|
"Cursor login store no longer exists",
|
|
);
|
|
expect(requests).toEqual([]);
|
|
});
|
|
});
|
|
|
|
it("discovers and uses auth.json after skipping an encrypted desktop token", async () => {
|
|
writeCursorStateDb(homeDir, {
|
|
"cursorAuth/accessToken": `cursor-safe-storage:v1:${Buffer.from("ciphertext").toString("base64")}`,
|
|
});
|
|
writeCursorAuthJson(homeDir, "cursor_cli_jwt");
|
|
expect(await discover()).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^store-[a-f0-9]{64}$/),
|
|
input: { store: "file", locator: join(homeDir, ".config", "cursor", "auth.json") },
|
|
},
|
|
]);
|
|
const authorizations: string[] = [];
|
|
fetchApi = async (_url, init) => {
|
|
authorizations.push(new Headers(init?.headers).get("Authorization") ?? "");
|
|
return jsonResponse({
|
|
planUsage: { totalSpend: "0", remaining: "100", limit: "100" },
|
|
billingCycleStart: null,
|
|
billingCycleEnd: null,
|
|
});
|
|
};
|
|
|
|
const cursor = findProvider(await service().listUsage(), "cursor");
|
|
expect(authorizations).toEqual(["Bearer cursor_cli_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<string, string>, 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<typeof fetchUsage>[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;
|
|
}
|
|
});
|