feat(cursor): Integrate Cursor IDE support with OAuth import token flow
- Add CursorExecutor for handling requests to the Cursor API using protobuf over HTTP/2. - Implement CursorAuthModal for user token import from local SQLite database. - Update provider models and constants to include Cursor as a supported provider. - Enhance API service with token validation and user info extraction from Cursor tokens. - Introduce utility functions for checksum generation and protobuf encoding/decoding for Cursor API interactions.
This commit is contained in:
@@ -142,6 +142,34 @@ export const KIRO_CONFIG = {
|
||||
authMethods: ["builder-id", "idc", "google", "github", "import"],
|
||||
};
|
||||
|
||||
// Cursor OAuth Configuration (Import Token from Cursor IDE)
|
||||
// Cursor stores credentials in SQLite database: state.vscdb
|
||||
// Keys: cursorAuth/accessToken, storage.serviceMachineId
|
||||
export const CURSOR_CONFIG = {
|
||||
// API endpoints
|
||||
apiEndpoint: "https://api2.cursor.sh",
|
||||
chatEndpoint: "/aiserver.v1.ChatService/StreamUnifiedChatWithTools",
|
||||
modelsEndpoint: "/aiserver.v1.AiService/GetDefaultModelNudgeData",
|
||||
// Additional endpoints
|
||||
api3Endpoint: "https://api3.cursor.sh", // Telemetry
|
||||
agentEndpoint: "https://agent.api5.cursor.sh", // Privacy mode
|
||||
agentNonPrivacyEndpoint: "https://agentn.api5.cursor.sh", // Non-privacy mode
|
||||
// Client metadata
|
||||
clientVersion: "0.48.6",
|
||||
clientType: "ide",
|
||||
// Token storage locations (for user reference)
|
||||
tokenStoragePaths: {
|
||||
linux: "~/.config/Cursor/User/globalStorage/state.vscdb",
|
||||
macos: "~/Library/Application Support/Cursor/User/globalStorage/state.vscdb",
|
||||
windows: "%APPDATA%\\Cursor\\User\\globalStorage\\state.vscdb",
|
||||
},
|
||||
// Database keys
|
||||
dbKeys: {
|
||||
accessToken: "cursorAuth/accessToken",
|
||||
machineId: "storage.serviceMachineId",
|
||||
},
|
||||
};
|
||||
|
||||
// OAuth timeout (5 minutes)
|
||||
export const OAUTH_TIMEOUT = 300000;
|
||||
|
||||
@@ -156,4 +184,5 @@ export const PROVIDERS = {
|
||||
OPENAI: "openai",
|
||||
GITHUB: "github",
|
||||
KIRO: "kiro",
|
||||
CURSOR: "cursor",
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ANTIGRAVITY_CONFIG,
|
||||
GITHUB_CONFIG,
|
||||
KIRO_CONFIG,
|
||||
CURSOR_CONFIG,
|
||||
} from "./constants/oauth";
|
||||
|
||||
// Provider configurations
|
||||
@@ -656,6 +657,22 @@ const PROVIDERS = {
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
cursor: {
|
||||
config: CURSOR_CONFIG,
|
||||
flowType: "import_token",
|
||||
// Cursor uses import token flow - tokens are extracted from local SQLite database
|
||||
// No OAuth flow needed, handled by /api/oauth/cursor/import route
|
||||
mapTokens: (tokens) => ({
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: null, // Cursor doesn't have public refresh endpoint
|
||||
expiresIn: tokens.expiresIn || 86400,
|
||||
providerSpecificData: {
|
||||
machineId: tokens.machineId,
|
||||
authMethod: "imported",
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
179
src/lib/oauth/services/cursor.js
Normal file
179
src/lib/oauth/services/cursor.js
Normal file
@@ -0,0 +1,179 @@
|
||||
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: ~/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')\"",
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export { AntigravityService } from "./antigravity.js";
|
||||
export { OpenAIService } from "./openai.js";
|
||||
export { GitHubService } from "./github.js";
|
||||
export { KiroService } from "./kiro.js";
|
||||
export { CursorService } from "./cursor.js";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user