fix(usage): skip encrypted Cursor desktop tokens (cursor-safe-storage:v1:)

This commit is contained in:
jasonhnd
2026-10-25 13:22:21 +00:00
parent 4845e1da58
commit 81ff587d1e
2 changed files with 93 additions and 7 deletions
@@ -250,6 +250,84 @@ describe("cursor usage source", () => {
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 });
+15 -7
View File
@@ -27,9 +27,11 @@ function toIsoStringOrNull(timestampMs: number): string | null {
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
}
// Cursor desktop stores auth in VS Code's ItemTable (state.vscdb). Modern builds keep
// the access token as a plain JWT string under `cursorAuth/accessToken`; older builds
// kept a JSON blob under `cursorAuthStatus`. Read it with node:sqlite so we don't
// Cursor desktop stores auth in VS Code's ItemTable (state.vscdb). The modern
// `cursorAuth/accessToken` value is a plain JWT or encrypted at rest. Any
// `cursor-safe-storage:` value (any version) is Electron safeStorage ciphertext
// and is skipped. Older builds kept a JSON blob under `cursorAuthStatus`.
// Read it with node:sqlite so we don't
// depend on a `sqlite3` CLI, which isn't installed by default on Windows (or on many
// Linux hosts) — a missing binary silently rendered Cursor usage unavailable.
// Headless hosts (VPS, cursor-agent only) have no desktop db; their session lives in
@@ -106,15 +108,21 @@ function readItemTableValue(db: CursorStateDatabase, key: string): string | null
return null;
}
function usableCursorToken(value: string | null | undefined): string | null {
const token = value?.trim();
// Electron safeStorage ciphertext cannot authenticate an API request.
return token && !token.startsWith("cursor-safe-storage:") ? token : null;
}
function cursorTokenFromDb(db: CursorStateDatabase): string | null {
const modern = readItemTableValue(db, CURSOR_ACCESS_TOKEN_KEY)?.trim();
const modern = usableCursorToken(readItemTableValue(db, CURSOR_ACCESS_TOKEN_KEY));
if (modern) return modern;
const legacy = readItemTableValue(db, CURSOR_LEGACY_AUTH_KEY);
if (legacy) {
try {
const parsed = CursorAuthStatusSchema.parse(JSON.parse(legacy));
if (parsed.accessToken) return parsed.accessToken;
return usableCursorToken(parsed.accessToken);
} catch {
// ignore a malformed legacy blob
}
@@ -149,7 +157,7 @@ async function readCursorTokenFromAuthJson(path: string): Promise<string | null>
if (!existsSync(path)) return null;
try {
const parsed = CursorAuthStatusSchema.parse(JSON.parse(await readFile(path, "utf8")));
return parsed.accessToken?.trim() || null;
return usableCursorToken(parsed.accessToken);
} catch {
return null;
}
@@ -230,7 +238,7 @@ export async function fetchUsage(
}
async function readToken(input: UsageInput): Promise<string | undefined> {
if (input.store === "env") return process.env[input.locator];
if (input.store === "env") return usableCursorToken(process.env[input.locator]) ?? undefined;
const token =
input.store === "sqlite"
? await readCursorTokenFromSqlite(input.locator)