fix(cursor): address remaining review items in auth module

- Enhanced extractUserInfo to return JWT exp claim for expiry detection
- Updated checkAuthStatus to use JWT exp when available, fallback to importedAt heuristic
- Added typeof validation for loadCredentials fields
- Added ENOENT detection for sqlite3 availability in queryStateDb
- Added deleteCredentials function for cleanup
- Updated PR #526 description to remove refreshToken mention
This commit is contained in:
Tam Nhu Tran
2026-02-12 01:07:25 +07:00
parent 7e4f08004c
commit b412ba2a9e
+56 -11
View File
@@ -60,7 +60,12 @@ function queryStateDb(dbPath: string, key: string): string | null {
{ encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'ignore'] }
).trim();
return result || null;
} catch {
} catch (err) {
// Check if sqlite3 is not installed
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
// sqlite3 not found - could log this if needed
return null;
}
return null;
}
}
@@ -145,7 +150,9 @@ export function validateToken(accessToken: string, machineId: string): boolean {
* 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 {
export function extractUserInfo(
accessToken: string
): { email?: string; userId?: string; exp?: number } | null {
try {
// Try to decode as JWT
const parts = accessToken.split('.');
@@ -161,6 +168,7 @@ export function extractUserInfo(accessToken: string): { email?: string; userId?:
return {
email: decoded.email || decoded.sub,
userId: decoded.sub || decoded.user_id,
exp: decoded.exp,
};
}
} catch {
@@ -219,6 +227,16 @@ export function loadCredentials(): CursorCredentials | null {
'authMethod' in parsed &&
'importedAt' in parsed
) {
// Type validation
if (
typeof parsed.accessToken !== 'string' ||
typeof parsed.machineId !== 'string' ||
typeof parsed.importedAt !== 'string' ||
(parsed.authMethod !== 'auto-detect' && parsed.authMethod !== 'manual')
) {
return null;
}
return parsed as CursorCredentials;
}
@@ -243,18 +261,27 @@ export function checkAuthStatus(): CursorAuthStatus {
return { authenticated: false };
}
// Calculate token age in hours
// Try to get token expiry from JWT exp claim
let tokenAge: number | undefined;
let expired = false;
const TOKEN_EXPIRY_HOURS = 24;
const userInfo = extractUserInfo(credentials.accessToken);
try {
const importedDate = new Date(credentials.importedAt);
const now = new Date();
tokenAge = Math.floor((now.getTime() - importedDate.getTime()) / (1000 * 60 * 60));
expired = tokenAge >= TOKEN_EXPIRY_HOURS;
} catch {
// Invalid date format
if (userInfo?.exp) {
// Use JWT exp claim (Unix timestamp in seconds)
const now = Math.floor(Date.now() / 1000);
expired = now >= userInfo.exp;
tokenAge = Math.floor((now - (userInfo.exp - 24 * 60 * 60)) / (60 * 60)); // Assume 24h token
} else {
// Fallback to importedAt heuristic
const TOKEN_EXPIRY_HOURS = 24;
try {
const importedDate = new Date(credentials.importedAt);
const now = new Date();
tokenAge = Math.floor((now.getTime() - importedDate.getTime()) / (1000 * 60 * 60));
expired = tokenAge >= TOKEN_EXPIRY_HOURS;
} catch {
// Invalid date format
}
}
return {
@@ -264,3 +291,21 @@ export function checkAuthStatus(): CursorAuthStatus {
expired,
};
}
/**
* Delete credentials file
*/
export function deleteCredentials(): boolean {
const credPath = getCredentialsPath();
if (!fs.existsSync(credPath)) {
return false;
}
try {
fs.unlinkSync(credPath);
return true;
} catch {
return false;
}
}