Files
OmniRoute/src/lib/oauth/services/cursor.ts
payne0420 ee228e6657 feat(cursor): surface Cursor Pro plan usage on provider-limits dashboard (#2128)
- Replace legacy getCursorUsage with dashboard API (cursor.com/api/dashboard/get-current-period-usage)
- Use WorkOS session cookie auth instead of Bearer token
- Surface 3 quota windows: Total, Auto + Composer, API
- Register cursor in USAGE_SUPPORTED_PROVIDERS
- Add fetchUserInfo() to resolve real email on import
- Remove ~170 lines of dead code (old fetcher + helpers)
- Add 6 comprehensive tests with fetch mocking

Co-authored-by: payne0420 <baboialex95@gmail.com>
2026-05-10 13:33:55 -03:00

220 lines
7.2 KiB
TypeScript

import { CURSOR_CONFIG } from "../constants/oauth";
import { getCursorUserAgent } from "@omniroute/open-sse/config/providerHeaderProfiles.ts";
/**
* Cursor IDE OAuth Service
* Supports Import Token method from Cursor IDE's local SQLite database
*
* Token Location:
* - Linux: ~/.config/Cursor/User/globalStorage/state.vscdb
* - macOS: /Users/<user>/Library/Application Support/Cursor/User/globalStorage/state.vscdb
* - Windows: %APPDATA%\Cursor\User\globalStorage\state.vscdb
*
* Database Keys:
* - cursorAuth/accessToken: The access token
* - storage.serviceMachineId: Machine ID for checksum
*/
export class CursorService {
config: any;
constructor() {
this.config = CURSOR_CONFIG;
}
/**
* Generate Cursor checksum (jyh cipher)
* Algorithm: XOR timestamp bytes with rolling key (initial 165), then base64 encode
* Format: {encoded_timestamp},{machineId}
*/
generateChecksum(machineId: string) {
const timestamp = Math.floor(Date.now() / 1000).toString();
let key = 165;
const encoded = [];
for (let i = 0; i < timestamp.length; i++) {
const charCode = timestamp.charCodeAt(i);
encoded.push(charCode ^ key);
key = (key + charCode) & 0xff; // Rolling key update
}
const base64Encoded = Buffer.from(encoded).toString("base64");
return `${base64Encoded},${machineId}`;
}
/**
* Build request headers for Cursor API
*/
buildHeaders(accessToken: string, machineId: string, ghostMode = false) {
const checksum = this.generateChecksum(machineId);
return {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/connect+proto",
"Connect-Protocol-Version": "1",
"User-Agent": getCursorUserAgent(this.config.clientVersion),
"x-cursor-client-version": this.config.clientVersion,
"x-cursor-client-type": this.config.clientType,
"x-cursor-client-os": this.detectOS(),
"x-cursor-client-arch": this.detectArch(),
"x-cursor-client-device-type": "desktop",
"x-cursor-user-agent": getCursorUserAgent(this.config.clientVersion),
"x-cursor-checksum": checksum,
"x-ghost-mode": ghostMode ? "true" : "false",
};
}
/**
* Detect OS for headers
*/
detectOS() {
if (typeof process !== "undefined") {
const platform = process.platform;
if (platform === "win32") return "windows";
if (platform === "darwin") return "macos";
return "linux";
}
return "linux";
}
/**
* Detect architecture for headers
*/
detectArch() {
if (typeof process !== "undefined") {
const arch = process.arch;
if (arch === "x64") return "x86_64";
if (arch === "arm64") return "aarch64";
return arch;
}
return "x86_64";
}
/**
* Validate and import token from Cursor IDE or cursor-agent CLI.
* Note: We skip API validation because Cursor API uses complex protobuf format.
* Token will be validated when actually used for requests.
* @param {string} accessToken - Access token from state.vscdb or auth.json
* @param {string} [machineId] - Machine ID from state.vscdb (optional for cursor-agent imports)
*/
async validateImportToken(accessToken: string, machineId?: string) {
// Basic validation
if (!accessToken || typeof accessToken !== "string") {
throw new Error("Access token is required");
}
// Token format validation (Cursor tokens are typically long strings)
if (accessToken.length < 50) {
throw new Error("Invalid token format. Token appears too short.");
}
// Machine ID format validation (only if provided — cursor-agent imports don't have one)
if (machineId) {
const uuidRegex = /^[a-f0-9-]{32,}$/i;
if (!uuidRegex.test(machineId.replace(/-/g, ""))) {
throw new Error("Invalid machine ID format. Expected UUID format.");
}
}
// Note: We don't validate against API because Cursor uses complex protobuf.
// Token will be validated when used for actual requests.
return {
accessToken,
machineId: machineId || null,
expiresIn: 86400, // Cursor tokens typically last 24 hours
authMethod: machineId ? "imported" : "cursor-agent",
};
}
/**
* Extract user info from token if possible
* Cursor tokens may contain encoded user info
*/
extractUserInfo(accessToken: string) {
try {
// Try to decode as JWT
const parts = accessToken.split(".");
if (parts.length === 3) {
let payload = parts[1];
while (payload.length % 4) {
payload += "=";
}
const decoded = JSON.parse(
Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString()
);
const email =
typeof decoded.email === "string" && decoded.email.includes("@") ? decoded.email : null;
return {
email,
userId: decoded.sub || decoded.user_id,
};
}
} catch {
// Token is not a JWT, that's okay
}
return null;
}
/**
* Fetch real user profile from cursor.com using the same WorkOS-session cookie
* format that powers the dashboard. Returns null on any failure so the import
* flow can fall back to whatever it can extract from the JWT.
*/
async fetchUserInfo(
accessToken: string,
userId: string
): Promise<{ email: string | null; name: string | null; sub: string | null } | null> {
if (!accessToken || !userId) return null;
try {
const response = await fetch("https://cursor.com/api/auth/me", {
method: "GET",
redirect: "manual",
headers: {
Cookie: `WorkosCursorSessionToken=${userId}::${accessToken}`,
Origin: "https://cursor.com",
Referer: "https://cursor.com/dashboard",
Accept: "application/json",
"User-Agent": getCursorUserAgent(this.config.clientVersion),
},
});
if (!response.ok) return null;
const data = (await response.json()) as Record<string, unknown>;
return {
email: typeof data.email === "string" ? data.email : null,
name: typeof data.name === "string" ? data.name : null,
sub: typeof data.sub === "string" ? data.sub : null,
};
} catch {
return null;
}
}
/**
* Get token storage path instructions for user
*/
getTokenStorageInstructions() {
return {
title: "How to get your Cursor token",
steps: [
"1. Open Cursor IDE and make sure you're logged in",
"2. Find the state.vscdb file:",
` - Linux: ${this.config.tokenStoragePaths.linux}`,
` - macOS: ${this.config.tokenStoragePaths.macos}`,
` - Windows: ${this.config.tokenStoragePaths.windows}`,
"3. Open the database with SQLite browser or CLI:",
" sqlite3 state.vscdb \"SELECT value FROM itemTable WHERE key='cursorAuth/accessToken'\"",
"4. Also get the machine ID:",
" sqlite3 state.vscdb \"SELECT value FROM itemTable WHERE key='storage.serviceMachineId'\"",
"5. Paste both values in the form below",
],
alternativeMethod: [
"Or use this one-liner to get both values:",
"sqlite3 state.vscdb \"SELECT key, value FROM itemTable WHERE key IN ('cursorAuth/accessToken', 'storage.serviceMachineId')\"",
],
};
}
}