justadri23's picture
Upload 17 files
f110811 verified
const os = require('os');
const zlib = require('zlib');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const $root = require('../proto/message.js');
const config = require('../config/config');
function generateCursorBody(messages, modelName) {
const instruction = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n')
const formattedMessages = messages
.filter(msg => msg.role !== 'system')
.map(msg => ({
content: msg.content,
role: msg.role === 'user' ? 1 : 2,
messageId: uuidv4(),
...(msg.role === 'user' ? { chatModeEnum: 1 } : {})
//...(msg.role !== 'user' ? { summaryId: uuidv4() } : {})
}));
const messageIds = formattedMessages.map(msg => {
const { role, messageId, summaryId } = msg;
return summaryId ? { role, messageId, summaryId } : { role, messageId };
});
const body = {
request:{
messages: formattedMessages,
unknown2: 1,
instruction: {
instruction: instruction
},
unknown4: 1,
model: {
name: modelName,
empty: '',
},
webTool: "",
unknown13: 1,
cursorSetting: {
name: "cursor\\aisettings",
unknown3: "",
unknown6: {
unknwon1: "",
unknown2: ""
},
unknown8: 1,
unknown9: 1
},
unknown19: 1,
//unknown22: 1,
conversationId: uuidv4(),
metadata: {
os: "win32",
arch: "x64",
version: "10.0.22631",
path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
timestamp: new Date().toISOString(),
},
unknown27: 0,
//unknown29: "",
messageIds: messageIds,
largeContext: 0,
unknown38: 0,
chatModeEnum: 1,
unknown47: "",
unknown48: 0,
unknown49: 0,
unknown51: 0,
unknown53: 1,
chatMode: "Ask"
}
};
const errMsg = $root.StreamUnifiedChatWithToolsRequest.verify(body);
if (errMsg) throw Error(errMsg);
const instance = $root.StreamUnifiedChatWithToolsRequest.create(body);
let buffer = $root.StreamUnifiedChatWithToolsRequest.encode(instance).finish();
let magicNumber = 0x00
if (formattedMessages.length >= 3){
buffer = zlib.gzipSync(buffer)
magicNumber = 0x01
}
const finalBody = Buffer.concat([
Buffer.from([magicNumber]),
Buffer.from(buffer.length.toString(16).padStart(8, '0'), 'hex'),
buffer
])
return finalBody
}
function chunkToUtf8String(chunk) {
const thinkingOutput = []
const textOutput = []
const buffer = Buffer.from(chunk, 'hex');
//console.log("Chunk buffer:", buffer.toString('hex'))
try {
for(let i = 0; i < buffer.length; i++){
const magicNumber = parseInt(buffer.subarray(i, i + 1).toString('hex'), 16)
const dataLength = parseInt(buffer.subarray(i + 1, i + 5).toString('hex'), 16)
const data = buffer.subarray(i + 5, i + 5 + dataLength)
//console.log("Parsed buffer:", magicNumber, dataLength, data.toString('hex'))
if (magicNumber == 0 || magicNumber == 1) {
const gunzipData = magicNumber == 0 ? data : zlib.gunzipSync(data)
const response = $root.StreamUnifiedChatWithToolsResponse.decode(gunzipData);
const thinking = response?.message?.thinking?.content
if (thinking !== undefined){
thinkingOutput.push(thinking)
//console.log(thinking)
}
const content = response?.message?.content
if (content !== undefined){
textOutput.push(content)
//console.log(content)
}
}
else if (magicNumber == 2 || magicNumber == 3) {
// Json message
const gunzipData = magicNumber == 2 ? data : zlib.gunzipSync(data)
const utf8 = gunzipData.toString('utf-8')
const message = JSON.parse(utf8)
if (message != null && (typeof message !== 'object' ||
(Array.isArray(message) ? message.length > 0 : Object.keys(message).length > 0))){
//results.push(utf8)
console.error(utf8)
}
}
else {
//console.log('Unknown magic number when parsing chunk response: ' + magicNumber)
}
i += 5 + dataLength - 1
}
} catch (err) {
console.log('Error parsing chunk response:', err)
}
const text = textOutput.join('');
const thinking = thinkingOutput.join('');
// Detectar mensaje de rate limit
const isRateLimited = checkForRateLimit(text);
return {
thinking,
text,
isRateLimited
}
}
// Verifica si el texto contiene el mensaje de rate limit de Cursor
function checkForRateLimit(text) {
// Versi贸n original (texto plano)
const rateLimitMessage = "You've hit your free requests limit. Upgrade to Pro for more usage, frontier models, Background Agents, and more.";
// Versi贸n con formato markdown (con asteriscos y enlace)
const markdownRateLimitMessage = "*You've hit your free requests limit. [Upgrade to Pro](https://www.cursor.com/api/auth/checkoutDeepControl?tier=pro) for more usage, frontier models, Background Agents, and more.*";
// URL espec铆fica que aparece en el mensaje de rate limit
const rateLimitUrl = "https://www.cursor.com/api/auth/checkoutDeepControl?tier=pro";
// Verificar cualquiera de las tres condiciones
return text.includes(rateLimitMessage) ||
text.includes(markdownRateLimitMessage) ||
text.includes(rateLimitUrl);
}
// Verifica si la respuesta contiene el error espec铆fico de actividad sospechosa
function checkForSuspiciousActivityError(response) {
if (!response || !response.body) return false;
try {
// Intentar parsear el cuerpo de la respuesta como JSON
const responseBody = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;
// Verificar si es el error espec铆fico
return responseBody?.error?.code === 'unauthenticated' &&
responseBody?.error?.details?.[0]?.debug?.error === 'ERROR_UNAUTHORIZED' &&
responseBody?.error?.details?.[0]?.debug?.details?.detail?.includes('Your request has been blocked as our system has detected suspicious activity from your account');
} catch (error) {
// Si hay un error al parsear el JSON, no es el error que buscamos
return false;
}
}
function generateHashed64Hex(input, salt = '') {
const hash = crypto.createHash('sha256');
hash.update(input + salt);
return hash.digest('hex');
}
function obfuscateBytes(byteArray) {
let t = 165;
for (let r = 0; r < byteArray.length; r++) {
byteArray[r] = (byteArray[r] ^ t) + (r % 256);
t = byteArray[r];
}
return byteArray;
}
function generateCursorChecksum(token) {
const machineId = generateHashed64Hex(token, 'machineId');
const macMachineId = generateHashed64Hex(token, 'macMachineId');
const timestamp = Math.floor(Date.now() / 1e6);
const byteArray = new Uint8Array([
(timestamp >> 40) & 255,
(timestamp >> 32) & 255,
(timestamp >> 24) & 255,
(timestamp >> 16) & 255,
(timestamp >> 8) & 255,
255 & timestamp,
]);
const obfuscatedBytes = obfuscateBytes(byteArray);
const encodedChecksum = Buffer.from(obfuscatedBytes).toString('base64');
return `${encodedChecksum}${machineId}/${macMachineId}`;
}
/**
* Obtiene el token de autenticaci贸n desde la configuraci贸n o headers
* Prioriza API keys autenticadas, luego AUTH_COOKIEs combinadas (entorno + base de datos), luego el header Authorization
* Si se proporciona un modelo y el modo privilegiado est谩 activado, selecciona el tipo de cookie adecuado
* @param {Object} req - Objeto request de Express
* @param {Object} config - Configuraci贸n de la aplicaci贸n
* @param {string} model - Modelo solicitado (opcional)
* @returns {string|null} Token de autenticaci贸n
*/
function getAuthToken(req, config, model = null) {
// Si hay una API key autenticada, usar el AUTH_COOKIE configurado (esto significa que la API key es v谩lida)
if (req.isAPIKeyAuth && config.authCookie) {
console.log('[AUTH] Usando AUTH_COOKIE para API key autenticada:', req.apiKey.name);
// Si estamos en modo privilegiado y se especific贸 un modelo, seleccionar el tipo de cookie adecuado
if (config.isPrivilegedMode && model) {
if (config.premiumModels.includes(model)) {
console.log(`[AUTH] Modelo ${model} requiere cookie premium`);
return config.getPremiumAuthCookies() || config.authCookie;
} else {
console.log(`[AUTH] Modelo ${model} usa cookie normal`);
return config.getNormalAuthCookies() || config.authCookie;
}
}
return config.authCookie;
}
// Si estamos en modo privilegiado y se especific贸 un modelo, seleccionar el tipo de cookie adecuado
if (config.isPrivilegedMode && model && config.authCookie) {
if (config.premiumModels.includes(model)) {
console.log(`[AUTH] Modelo ${model} requiere cookie premium`);
return config.getPremiumAuthCookies() || config.authCookie;
} else {
console.log(`[AUTH] Modelo ${model} usa cookie normal`);
return config.getNormalAuthCookies() || config.authCookie;
}
}
// Priorizar AUTH_COOKIEs combinadas (entorno + base de datos)
if (config.authCookie) {
console.log('[AUTH] Usando AUTH_COOKIEs combinadas (entorno + base de datos)');
return config.authCookie;
}
// Fallback al header Authorization (si no es una API key v谩lida)
if (!req.isAPIKeyAuth) {
const bearerToken = req.headers.authorization?.replace('Bearer ', '');
if (bearerToken && !bearerToken.startsWith('sk-')) {
console.log('[AUTH] Usando token del header Authorization');
return bearerToken;
}
}
console.log('[AUTH] No se encontr贸 token de autenticaci贸n v谩lido');
return null;
}
/**
* Parsea el tiempo de rotaci贸n de cookies (formato: 5s, 5m, 5h)
* @param {string} timeString - Cadena de tiempo con formato (ej: "5m")
* @returns {number} Tiempo en milisegundos
*/
function parseRotationTime(timeString) {
const match = timeString.match(/^(\d+)([smh])$/);
if (!match) return 5 * 60 * 1000; // Default: 5 minutos
const value = parseInt(match[1]);
const unit = match[2];
switch (unit) {
case 's': return value * 1000; // segundos
case 'm': return value * 60 * 1000; // minutos
case 'h': return value * 60 * 60 * 1000; // horas
default: return 5 * 60 * 1000; // Default: 5 minutos
}
}
// Variables para seguimiento de rotaci贸n de cookies
let lastUsedTokenIndex = -1;
let lastTokenTime = 0;
let failedTokens = new Set(); // Conjunto para almacenar 铆ndices de tokens fallidos
let lastFailedResetTime = 0; // Tiempo de 煤ltimo reinicio de tokens fallidos
const FAILED_TOKEN_RESET_TIME = 1 * 60 * 1000; // 1 minuto para reiniciar tokens fallidos
let totalAttempts = 0; // Contador de intentos totales en el ciclo actual
const MAX_TOTAL_ATTEMPTS = 50; // M谩ximo n煤mero de intentos totales para evitar bucles infinitos
const ROTATION_DELAY = 2000; // Delay de 2 segundos entre rotaci贸n de cuentas
/**
* Procesa el token de autenticaci贸n para extraer la parte relevante
* Implementa rotaci贸n de cookies si est谩 habilitado en la configuraci贸n
* @param {string} authToken - Token de autenticaci贸n crudo
* @param {boolean} forceRotate - Forzar rotaci贸n a la siguiente cookie (para manejo de errores)
* @returns {string} Token procesado
*/
function processAuthToken(authToken, forceRotate = false) {
if (!authToken) return null;
// Dividir por comas para manejar m煤ltiples tokens
const keys = authToken.split(',').map((key) => key.trim());
let tokenIndex;
// Limpiar tokens fallidos si ha pasado el tiempo de reinicio
const now = Date.now();
if (now - lastFailedResetTime > FAILED_TOKEN_RESET_TIME) {
failedTokens.clear();
totalAttempts = 0; // Reiniciar contador de intentos totales
lastFailedResetTime = now;
}
// Aplicar rotaci贸n si est谩 habilitada y hay m煤ltiples cookies
if (keys.length > 1) {
// Si se fuerza la rotaci贸n o est谩 habilitada la rotaci贸n autom谩tica
if (forceRotate || (config.cookieRotation && config.cookieRotation.enabled)) {
const rotationTime = config.cookieRotation ? parseRotationTime(config.cookieRotation.time) : 5 * 60 * 1000;
if (forceRotate || lastUsedTokenIndex === -1 || now - lastTokenTime >= rotationTime) {
// Incrementar contador de intentos totales
totalAttempts++;
// Verificar si hemos alcanzado el l铆mite m谩ximo de intentos
if (totalAttempts > MAX_TOTAL_ATTEMPTS) {
console.log(`[AUTH] Alcanzado el l铆mite m谩ximo de ${MAX_TOTAL_ATTEMPTS} intentos totales, reiniciando estado`);
failedTokens.clear();
totalAttempts = 1; // Reiniciar pero contar este intento
}
// Buscar el siguiente token que no haya fallado
let attemptsCount = 0;
let nextIndex = lastUsedTokenIndex;
do {
// Rotar al siguiente token
nextIndex = (nextIndex + 1) % keys.length;
attemptsCount++;
// Si hemos probado todas las cookies y todas han fallado, implementar bucle circular
if (attemptsCount > keys.length) {
console.log(`[AUTH] Todas las cookies han fallado, implementando bucle circular (intento total: ${totalAttempts})`);
// En lugar de reiniciar el estado, volvemos a intentar con la primera cookie
// pero mantenemos el registro de intentos para evitar bucles infinitos
nextIndex = 0; // Volver a la primera cookie
break;
}
} while (failedTokens.has(nextIndex));
tokenIndex = nextIndex;
lastUsedTokenIndex = tokenIndex;
lastTokenTime = now;
console.log(`[AUTH] ${forceRotate ? 'Forzando rotaci贸n' : 'Rotando'} a cookie #${tokenIndex + 1} de ${keys.length} (intento total: ${totalAttempts})`);
} else {
// Usar el mismo token si no ha pasado el tiempo de rotaci贸n
tokenIndex = lastUsedTokenIndex;
console.log(`[AUTH] Usando cookie #${tokenIndex + 1} (rotaci贸n cada ${config.cookieRotation ? config.cookieRotation.time : '5m'})`);
}
} else {
// Sin rotaci贸n, seleccionar un token aleatorio (comportamiento original)
tokenIndex = Math.floor(Math.random() * keys.length);
console.log(`[AUTH] Seleccionando cookie aleatoria #${tokenIndex + 1} de ${keys.length}`);
}
} else {
// Sin rotaci贸n, seleccionar un token aleatorio (comportamiento original)
tokenIndex = Math.floor(Math.random() * keys.length);
if (keys.length > 1) {
console.log(`[AUTH] Seleccionando cookie aleatoria #${tokenIndex + 1} de ${keys.length}`);
}
}
let processedToken = keys[tokenIndex];
// Procesar formato de token
if (processedToken && processedToken.includes('%3A%3A')) {
processedToken = processedToken.split('%3A%3A')[1];
} else if (processedToken && processedToken.includes('::')) {
processedToken = processedToken.split('::')[1];
}
return processedToken?.trim();
}
/**
* Marca un token como fallido para que no se use en la pr贸xima rotaci贸n
* @param {string} authToken - Token de autenticaci贸n que fall贸
*/
function markTokenAsFailed(authToken) {
if (!authToken) return;
const keys = authToken.split(',').map((key) => key.trim());
if (keys.length <= 1) return; // No hay alternativas para rotar
if (lastUsedTokenIndex >= 0 && lastUsedTokenIndex < keys.length) {
console.log(`[AUTH] Marcando cookie #${lastUsedTokenIndex + 1} como fallida`);
failedTokens.add(lastUsedTokenIndex);
}
}
/**
* Obtiene el siguiente token disponible despu茅s de un fallo
* @param {string} authToken - Token de autenticaci贸n crudo
* @returns {Promise<string>} Promesa con el siguiente token procesado
*/
async function getNextAuthToken(authToken) {
// Esperar el tiempo de delay configurado antes de rotar
console.log(`[AUTH] Esperando ${ROTATION_DELAY/1000} segundos antes de rotar a la siguiente cuenta...`);
await new Promise(resolve => setTimeout(resolve, ROTATION_DELAY));
return processAuthToken(authToken, true);
}
/**
* Maneja respuestas con rate limit o error de actividad sospechosa,
* marcando la cookie actual como rate-limited y rotando a la siguiente cookie disponible
* @param {string} authToken - Token de autenticaci贸n crudo
* @param {boolean} isRateLimited - Si la respuesta est谩 rate-limited
* @param {boolean} isSuspiciousActivity - Si la respuesta contiene el error de actividad sospechosa
* @param {string} model - Modelo solicitado (para usar premium si es necesario)
* @param {Object} config - Configuraci贸n de la aplicaci贸n
* @returns {Promise<Object>} Promesa con el resultado, nuevo token y si se debe reintentar
*/
async function handleRateLimitedResponse(authToken, isRateLimited, isSuspiciousActivity = false, model = null, config = null) {
if (!isRateLimited && !isSuspiciousActivity) {
return {
shouldRetry: false,
newToken: authToken,
usePremium: false
};
}
// Mensaje de log seg煤n el tipo de error
if (isRateLimited) {
console.log('[AUTH] Detectado mensaje de rate limit en la respuesta');
} else if (isSuspiciousActivity) {
console.log('[AUTH] Detectado error de actividad sospechosa en la respuesta');
}
// Obtener el ID de la cookie actual
const authCookieDB = require('../config/auth_cookies');
const keys = authToken.split(',').map(key => key.trim());
if (lastUsedTokenIndex >= 0 && lastUsedTokenIndex < keys.length) {
const currentToken = keys[lastUsedTokenIndex];
// Buscar la cookie en la base de datos por su valor
for (const cookie of authCookieDB.getAllAuthCookies()) {
// Comparar con el valor completo y tambi茅n con la parte procesada
const processedValue = processAuthToken(cookie.value);
const currentProcessed = processAuthToken(currentToken);
if (cookie.value === currentToken || processedValue === currentProcessed) {
if (isRateLimited) {
console.log(`[AUTH] Marcando cookie ${cookie.name} (${cookie.id.substring(0, 8)}...) como rate-limited por 12 horas`);
authCookieDB.markAsRateLimited(cookie.id);
} else if (isSuspiciousActivity) {
console.log(`[AUTH] Cookie ${cookie.name} (${cookie.id.substring(0, 8)}...) ha encontrado error de actividad sospechosa`);
}
break;
}
}
}
// Si es un error de actividad sospechosa y estamos en modo privilegiado, intentar con una cookie premium
if (isSuspiciousActivity && config && config.isPrivilegedMode) {
console.log('[AUTH] Intentando usar cookie premium como fallback para el error de actividad sospechosa');
const premiumToken = config.getPremiumAuthCookies();
if (premiumToken) {
console.log('[AUTH] Usando cookie premium como fallback');
return {
shouldRetry: true,
newToken: premiumToken,
usePremium: true
};
}
}
// Para rate limit normal o si no hay cookies premium disponibles
// Marcar el token como fallido en la rotaci贸n interna
markTokenAsFailed(authToken);
// Obtener el siguiente token (con delay incorporado)
const nextToken = await getNextAuthToken(authToken);
// Siempre reintentar, incluso si todas las cookies est谩n rate-limited
// El bucle circular se implementa en processAuthToken
console.log('[AUTH] Rotando a la siguiente cookie disponible (bucle circular)');
return {
shouldRetry: true,
newToken: authToken, // El token original, la rotaci贸n se maneja internamente
usePremium: false
};
}
/**
* Funci贸n para probar una cookie rate-limited
* @param {string} cookieId - ID de la cookie a probar
* @returns {Promise<boolean>} True si la cookie est谩 funcionando, false si sigue rate-limited
*/
async function testRateLimitedCookie(cookieId) {
const authCookieDB = require('../config/auth_cookies');
const cookie = authCookieDB.getAuthCookieById(cookieId);
if (!cookie) {
console.log(`[AUTH] No se encontr贸 la cookie con ID ${cookieId}`);
return false;
}
console.log(`[AUTH] Probando cookie rate-limited: ${cookie.name} (${cookieId.substring(0, 8)}...)`);
try {
// Crear un mensaje simple para probar
const messages = [
{ role: "user", content: "Hello, are you working?" }
];
// Usar el modelo claude-3.7-sonnet para la prueba
const modelName = "claude-3.7-sonnet";
// Generar el cuerpo de la solicitud
const cursorBody = generateCursorBody(messages, modelName);
// Configurar el token para la solicitud
const processedToken = processAuthToken(cookie.value);
const cursorChecksum = generateCursorChecksum(processedToken.trim());
const { v5: uuidv5, v4: uuidv4 } = require('uuid');
const sessionid = uuidv5(processedToken, uuidv5.DNS);
const clientKey = generateHashed64Hex(processedToken);
// Realizar la solicitud
const response = await fetch('https://api2.cursor.sh/aiserver.v1.ChatService/StreamUnifiedChatWithTools', {
method: 'POST',
headers: {
'authorization': `Bearer ${processedToken}`,
'connect-accept-encoding': 'gzip',
'connect-content-encoding': 'gzip',
'connect-protocol-version': '1',
'content-type': 'application/connect+proto',
'user-agent': 'connect-es/1.6.1',
'x-amzn-trace-id': `Root=${uuidv4()}`,
'x-client-key': clientKey,
'x-cursor-checksum': cursorChecksum,
'x-cursor-client-version': "0.48.7",
'x-cursor-config-version': uuidv4(),
'x-cursor-timezone': 'Asia/Shanghai',
'x-ghost-mode': 'true',
'x-request-id': uuidv4(),
'x-session-id': sessionid,
'Host': 'api2.cursor.sh'
},
body: cursorBody,
timeout: {
connect: 5000,
read: 30000
}
});
// Procesar la respuesta
if (response.status !== 200) {
console.log(`[AUTH] La prueba de la cookie fall贸 con estado: ${response.status}`);
return false;
}
// Leer el primer chunk para verificar si hay rate limit
const chunk = await response.body.getReader().read();
if (chunk.done) {
console.log('[AUTH] No se recibi贸 respuesta en la prueba de la cookie');
return false;
}
const { isRateLimited } = chunkToUtf8String(Buffer.from(chunk.value).toString('hex'));
if (isRateLimited) {
console.log('[AUTH] La cookie sigue rate-limited');
// Actualizar el tiempo de la pr贸xima prueba
authCookieDB.updateNextTestTime(cookieId);
return false;
} else {
console.log('[AUTH] La cookie ha sido reactivada');
// Reactivar la cookie
authCookieDB.clearRateLimit(cookieId);
return true;
}
} catch (error) {
console.error('[AUTH] Error al probar la cookie:', error.message);
// Actualizar el tiempo de la pr贸xima prueba
authCookieDB.updateNextTestTime(cookieId);
return false;
}
}
module.exports = {
generateCursorBody,
chunkToUtf8String,
generateHashed64Hex,
generateCursorChecksum,
getAuthToken,
processAuthToken,
parseRotationTime,
markTokenAsFailed,
getNextAuthToken,
checkForRateLimit,
checkForSuspiciousActivityError,
handleRateLimitedResponse,
testRateLimitedCookie
};