mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 06:15:03 +00:00
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:
1 parent
8244162ff3
commit
56270d99f3
2 files changed
+387
No files matched your search
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user