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