Spaces:
Runtime error
Runtime error
| 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')\"", | |
| ], | |
| }; | |
| } | |
| } | |