feat(cursor): add auth module with SQLite auto-detect and types

- Add CursorCredentials, CursorAuthStatus, and related types
- Implement autoDetectTokens() using sqlite3 CLI (no native deps)
- Add validateToken() for token format validation
- Add extractUserInfo() for JWT decoding
- Add saveCredentials/loadCredentials using getCcsDir()
- Add checkAuthStatus() for auth verification
- Platform-specific state.vscdb paths (Linux/macOS/Windows)
- Graceful error handling for missing database/tokens

Refs: #519 (sub-task of #517)
This commit is contained in:
Tam Nhu Tran
2026-02-11 17:55:15 +07:00
parent 8244162ff3
commit 56270d99f3
2 changed files with 387 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
/**
* Cursor IDE Authentication Handler
*
* Handles token import and authentication for Cursor IDE integration.
* Supports auto-detection from Cursor's 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: Access token
* - storage.serviceMachineId: Machine ID for checksum
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { CursorCredentials, CursorAuthStatus, AutoDetectResult } from './types';
import { getCcsDir } from '../utils/config-manager';
/**
* Get platform-specific path to Cursor's state.vscdb
*/
export function getTokenStoragePath(): string {
const platform = process.platform;
const home = os.homedir();
if (platform === 'win32') {
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
return path.join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb');
} else if (platform === 'darwin') {
return path.join(
home,
'Library',
'Application Support',
'Cursor',
'User',
'globalStorage',
'state.vscdb'
);
} else {
// Linux
return path.join(home, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb');
}
}
/**
* Query Cursor's SQLite database using sqlite3 CLI
*/
function queryStateDb(dbPath: string, key: string): string | null {
try {
const result = execSync(
`sqlite3 "${dbPath}" "SELECT value FROM itemTable WHERE key='${key}'" 2>/dev/null`,
{ encoding: 'utf8', timeout: 5000 }
).trim();
return result || null;
} catch {
return null;
}
}
/**
* Auto-detect tokens from Cursor's SQLite database
*/
export function autoDetectTokens(): AutoDetectResult {
const dbPath = getTokenStoragePath();
// Check if database exists
if (!fs.existsSync(dbPath)) {
return {
found: false,
error:
'Cursor state database not found. Make sure Cursor IDE is installed and you are logged in.',
};
}
// Try to query access token
const accessToken = queryStateDb(dbPath, 'cursorAuth/accessToken');
if (!accessToken) {
return {
found: false,
error: 'Access token not found in database. Please log in to Cursor IDE first.',
};
}
// Try to query machine ID
const machineId = queryStateDb(dbPath, 'storage.serviceMachineId');
if (!machineId) {
return {
found: false,
error: 'Machine ID not found in database.',
};
}
return {
found: true,
accessToken,
machineId,
};
}
/**
* Validate token and machine ID format
*/
export function validateToken(accessToken: string, machineId: string): boolean {
// Basic validation
if (!accessToken || typeof accessToken !== 'string') {
return false;
}
if (!machineId || typeof machineId !== 'string') {
return false;
}
// Token format validation (Cursor tokens are typically long strings)
if (accessToken.length < 50) {
return false;
}
// Machine ID format validation (should be UUID-like)
const uuidRegex = /^[a-f0-9-]{32,}$/i;
if (!uuidRegex.test(machineId.replace(/-/g, ''))) {
return false;
}
return true;
}
/**
* Extract user info from token if possible
* Cursor tokens may contain encoded user info as JWT
*/
export function extractUserInfo(accessToken: string): { email?: string; userId?: string } | null {
try {
// Try to decode as JWT
const parts = accessToken.split('.');
if (parts.length === 3) {
let payload = parts[1];
// Add padding if needed
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 path to credentials file
*/
export function getCredentialsPath(): string {
return path.join(getCcsDir(), 'cursor', 'credentials.json');
}
/**
* Save credentials to CCS config directory
*/
export function saveCredentials(credentials: CursorCredentials): void {
const credPath = getCredentialsPath();
const dir = path.dirname(credPath);
// Ensure directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Write credentials
fs.writeFileSync(credPath, JSON.stringify(credentials, null, 2), 'utf8');
}
/**
* Load credentials from CCS config directory
*/
export function loadCredentials(): CursorCredentials | null {
const credPath = getCredentialsPath();
if (!fs.existsSync(credPath)) {
return null;
}
try {
const raw = fs.readFileSync(credPath, 'utf8');
const parsed: unknown = JSON.parse(raw);
// Basic validation
if (
typeof parsed === 'object' &&
parsed !== null &&
'accessToken' in parsed &&
'machineId' in parsed &&
'authMethod' in parsed &&
'importedAt' in parsed
) {
return parsed as CursorCredentials;
}
return null;
} catch {
return null;
}
}
/**
* Check authentication status
*/
export function checkAuthStatus(): CursorAuthStatus {
const credentials = loadCredentials();
if (!credentials) {
return { authenticated: false };
}
// Validate credentials are still valid format
if (!validateToken(credentials.accessToken, credentials.machineId)) {
return { authenticated: false };
}
// Calculate token age in hours
let tokenAge: number | undefined;
try {
const importedDate = new Date(credentials.importedAt);
const now = new Date();
tokenAge = Math.floor((now.getTime() - importedDate.getTime()) / (1000 * 60 * 60));
} catch {
// Invalid date format
}
return {
authenticated: true,
credentials,
tokenAge,
};
}
+141
View File
@@ -0,0 +1,141 @@
/**
* Cursor IDE Type Definitions
*
* TypeScript interfaces for the Cursor module.
*/
/**
* Cursor authentication credentials
*/
export interface CursorCredentials {
/** Access token from Cursor IDE */
accessToken: string;
/** Machine ID for checksum generation */
machineId: string;
/** User email (if available from token) */
email?: string;
/** User ID (if available from token) */
userId?: string;
/** How credentials were obtained */
authMethod: 'auto-detect' | 'manual';
/** ISO datetime when credentials were imported */
importedAt: string;
}
/**
* Cursor authentication status
*/
export interface CursorAuthStatus {
/** Whether user is authenticated */
authenticated: boolean;
/** Current credentials (if authenticated) */
credentials?: CursorCredentials;
/** Hours since credentials were imported (if available) */
tokenAge?: number;
}
/**
* Cursor daemon/process status
*/
export interface CursorDaemonStatus {
/** Whether daemon is running */
running: boolean;
/** Port number daemon is listening on */
port: number;
/** Process ID (if available) */
pid?: number;
}
/**
* Cursor AI model
*/
export interface CursorModel {
/** Model ID */
id: string;
/** Display name */
name: string;
/** Provider (e.g., 'openai', 'anthropic') */
provider: string;
/** Whether this is the default model */
isDefault?: boolean;
}
/**
* Message role
*/
export type MessageRole = 'user' | 'assistant';
/**
* Cursor message for protobuf
*/
export interface CursorMessage {
/** Message role */
role: MessageRole;
/** Message content */
content: string;
/** Tool calls (if any) */
tool_calls?: CursorToolCall[];
/** Tool results (if any) */
tool_results?: CursorToolResult[];
}
/**
* Cursor tool call
*/
export interface CursorToolCall {
/** Unique ID for this tool call */
id: string;
/** Type of tool call */
type: 'function';
/** Function details */
function: {
/** Function name */
name: string;
/** JSON-encoded arguments */
arguments: string;
};
/** Whether this is the last tool call in sequence */
isLast?: boolean;
}
/**
* Cursor tool result
*/
export interface CursorToolResult {
/** ID of the tool call this result is for */
tool_call_id: string;
/** Tool name */
name: string;
/** Result index */
index: number;
/** Raw arguments */
raw_args: string;
}
/**
* Result from protobuf extraction
*/
export interface ProtobufExtractResult {
/** Extracted text content */
text: string | null;
/** Error message (if extraction failed) */
error: string | null;
/** Extracted tool call (if any) */
toolCall: CursorToolCall | null;
/** Thinking/reasoning content (if any) */
thinking: string | null;
}
/**
* Auto-detection result
*/
export interface AutoDetectResult {
/** Whether tokens were found */
found: boolean;
/** Access token (if found) */
accessToken?: string;
/** Machine ID (if found) */
machineId?: string;
/** Error message (if detection failed) */
error?: string;
}