File size: 5,636 Bytes
88c4c60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { CURSOR_CONFIG } from "../constants/oauth.js";

/**
 * 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 {
  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) {
    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, machineId, ghostMode = false) {
    const checksum = this.generateChecksum(machineId);

    return {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/connect+proto",
      "Connect-Protocol-Version": "1",
      "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-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
   * 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
   * @param {string} machineId - Machine ID from state.vscdb
   */
  async validateImportToken(accessToken, machineId) {
    // Basic validation
    if (!accessToken || typeof accessToken !== "string") {
      throw new Error("Access token is required");
    }

    if (!machineId || typeof machineId !== "string") {
      throw new Error("Machine ID 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 (should be UUID-like)
    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,
      expiresIn: 86400, // Cursor tokens typically last 24 hours
      authMethod: "imported",
    };
  }

  /**
   * Extract user info from token if possible
   * Cursor tokens may contain encoded user info
   */
  extractUserInfo(accessToken) {
    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()
        );
        return {
          email: decoded.email || decoded.sub,
          userId: decoded.sub || decoded.user_id,
        };
      }
    } catch {
      // Token is not a JWT, that's okay
    }

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