9router/src/lib/oauth/services/cursor.js
2026-02-05 11:02:50 +07:00

179 lines
5.5 KiB
JavaScript

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