| import { CURSOR_CONFIG } from "../constants/oauth";
|
| import { getCursorUserAgent } from "@omniroute/open-sse/config/providerHeaderProfiles.ts";
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| export class CursorService {
|
| config: any;
|
|
|
| constructor() {
|
| this.config = CURSOR_CONFIG;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| 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;
|
| }
|
|
|
| const base64Encoded = Buffer.from(encoded).toString("base64");
|
| return `${base64Encoded},${machineId}`;
|
| }
|
|
|
| |
| |
|
|
| 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",
|
| };
|
| }
|
|
|
| |
| |
|
|
| detectOS() {
|
| if (typeof process !== "undefined") {
|
| const platform = process.platform;
|
| if (platform === "win32") return "windows";
|
| if (platform === "darwin") return "macos";
|
| return "linux";
|
| }
|
| return "linux";
|
| }
|
|
|
| |
| |
|
|
| 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";
|
| }
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| async validateImportToken(accessToken: string, machineId?: string) {
|
|
|
| if (!accessToken || typeof accessToken !== "string") {
|
| throw new Error("Access token is required");
|
| }
|
|
|
|
|
| if (accessToken.length < 50) {
|
| throw new Error("Invalid token format. Token appears too short.");
|
| }
|
|
|
|
|
| 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.");
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| return {
|
| accessToken,
|
| machineId: machineId || null,
|
| expiresIn: 86400,
|
| authMethod: machineId ? "imported" : "cursor-agent",
|
| };
|
| }
|
|
|
| |
| |
| |
|
|
| extractUserInfo(accessToken: string) {
|
| try {
|
|
|
| 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 {
|
|
|
| }
|
|
|
| return null;
|
| }
|
|
|
| |
| |
| |
| |
|
|
| 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;
|
| }
|
| }
|
|
|
| |
| |
|
|
| 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')\"",
|
| ],
|
| };
|
| }
|
| }
|
|
|