225 lines
6.0 KiB
TypeScript
225 lines
6.0 KiB
TypeScript
/**
|
|
* Claude Web Auto-Refresh Service
|
|
*
|
|
* Provides automatic cf_clearance token management:
|
|
* - Caches tokens for 55 minutes (typical Cloudflare expiry is 1 hour)
|
|
* - Auto-solves Turnstile challenges using Playwright
|
|
* - Injects fresh tokens into existing session cookies
|
|
* - Handles retry logic for failed requests
|
|
*/
|
|
|
|
import { getCfClearanceToken, getCacheStatus } from "./claudeTurnstileSolver.ts";
|
|
|
|
export interface CookieRefreshOptions {
|
|
force?: boolean;
|
|
maxRetries?: number;
|
|
timeout?: number;
|
|
log?: any;
|
|
}
|
|
|
|
export interface CookieRefreshResult {
|
|
cookie: string;
|
|
cfClearanceInjected: boolean;
|
|
attempt: number;
|
|
}
|
|
|
|
/**
|
|
* Inject cf_clearance into cookie string
|
|
*/
|
|
export function injectCfClearance(existingCookie: string, cfClearanceToken: string): string {
|
|
if (!existingCookie || !existingCookie.trim()) {
|
|
return `cf_clearance=${cfClearanceToken}`;
|
|
}
|
|
|
|
// Check if cf_clearance already exists
|
|
if (existingCookie.includes("cf_clearance=")) {
|
|
// Replace existing token
|
|
return existingCookie.replace(/cf_clearance=[^;]+/, `cf_clearance=${cfClearanceToken}`);
|
|
}
|
|
|
|
// Append new token
|
|
return `${existingCookie.trim()}; cf_clearance=${cfClearanceToken}`;
|
|
}
|
|
|
|
/**
|
|
* Refresh cf_clearance token in cookie
|
|
*/
|
|
export async function refreshCookie(
|
|
existingCookie: string,
|
|
options?: CookieRefreshOptions
|
|
): Promise<CookieRefreshResult> {
|
|
const { force = false, log } = options || {};
|
|
|
|
try {
|
|
log?.info?.("CLAUDE-WEB-AUTO-REFRESH", "Fetching fresh cf_clearance...");
|
|
|
|
const cfClearanceToken = await getCfClearanceToken({ force });
|
|
const newCookie = injectCfClearance(existingCookie, cfClearanceToken);
|
|
|
|
log?.info?.("CLAUDE-WEB-AUTO-REFRESH", "cf_clearance token injected successfully");
|
|
|
|
return {
|
|
cookie: newCookie,
|
|
cfClearanceInjected: true,
|
|
attempt: 1,
|
|
};
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
log?.error?.("CLAUDE-WEB-AUTO-REFRESH", `Failed to refresh cf_clearance: ${message}`);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current cache status for diagnostics
|
|
*/
|
|
export function getCacheInfo(): {
|
|
hasCached: boolean;
|
|
expiresIn?: number;
|
|
message: string;
|
|
} {
|
|
const status = getCacheStatus();
|
|
|
|
if (!status.hasCached) {
|
|
return {
|
|
hasCached: false,
|
|
message: "No cached cf_clearance",
|
|
};
|
|
}
|
|
|
|
const minutes = Math.floor((status.expiresIn || 0) / 60000);
|
|
const seconds = Math.floor(((status.expiresIn || 0) % 60000) / 1000);
|
|
|
|
return {
|
|
hasCached: true,
|
|
expiresIn: status.expiresIn,
|
|
message: `cf_clearance cached: expires in ${minutes}m${seconds}s`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Middleware for fetch interceptor
|
|
* Usage: Wrap fetch calls to auto-refresh on 403/401
|
|
*/
|
|
export async function fetchWithAutoRefresh<T>(
|
|
fetchFn: (cookie: string) => Promise<T>,
|
|
initialCookie: string,
|
|
options?: CookieRefreshOptions
|
|
): Promise<{ result: T; cookie: string; refreshed: boolean }> {
|
|
const maxRetries = options?.maxRetries ?? 2;
|
|
let attempt = 0;
|
|
let currentCookie = initialCookie;
|
|
let lastError: Error | null = null;
|
|
|
|
while (attempt < maxRetries) {
|
|
attempt++;
|
|
|
|
try {
|
|
const result = await fetchFn(currentCookie);
|
|
return {
|
|
result,
|
|
cookie: currentCookie,
|
|
refreshed: attempt > 1,
|
|
};
|
|
} catch (error) {
|
|
lastError = error as Error;
|
|
|
|
// Check if error is 403/401
|
|
const isAuthError = lastError.message?.includes("403") || lastError.message?.includes("401");
|
|
|
|
if (!isAuthError || attempt >= maxRetries) {
|
|
throw lastError;
|
|
}
|
|
|
|
options?.log?.warn?.(
|
|
"CLAUDE-WEB-AUTO-REFRESH",
|
|
`Auth error detected (attempt ${attempt}/${maxRetries}), refreshing cf_clearance...`
|
|
);
|
|
|
|
try {
|
|
const refresh = await refreshCookie(currentCookie, {
|
|
...options,
|
|
force: attempt > 1,
|
|
});
|
|
currentCookie = refresh.cookie;
|
|
} catch (refreshError) {
|
|
options?.log?.error?.("CLAUDE-WEB-AUTO-REFRESH", "Refresh failed");
|
|
throw refreshError;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error("Max retries exceeded");
|
|
}
|
|
|
|
/**
|
|
* Create a middleware function for fetch wrappers
|
|
*/
|
|
export function createAutoRefreshMiddleware(options?: CookieRefreshOptions) {
|
|
return async (
|
|
fetch: (url: string, init?: any) => Promise<Response>,
|
|
url: string,
|
|
init?: any
|
|
): Promise<Response> => {
|
|
const { log = options?.log } = options || {};
|
|
const originalCookie = init?.headers?.Cookie || "";
|
|
let currentCookie = originalCookie;
|
|
let attempt = 0;
|
|
const maxRetries = options?.maxRetries ?? 2;
|
|
|
|
while (attempt < maxRetries) {
|
|
attempt++;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...init,
|
|
headers: {
|
|
...init?.headers,
|
|
Cookie: currentCookie,
|
|
},
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
return response;
|
|
}
|
|
|
|
// 403 or 401 - try refresh
|
|
if ((response.status === 403 || response.status === 401) && attempt < maxRetries) {
|
|
log?.warn?.(
|
|
"CLAUDE-WEB-AUTO-REFRESH",
|
|
`HTTP ${response.status} - refreshing cf_clearance (attempt ${attempt}/${maxRetries})`
|
|
);
|
|
|
|
try {
|
|
const refresh = await refreshCookie(currentCookie, {
|
|
...options,
|
|
force: attempt > 1,
|
|
log,
|
|
});
|
|
currentCookie = refresh.cookie;
|
|
continue; // Retry with new cookie
|
|
} catch (error) {
|
|
log?.error?.("CLAUDE-WEB-AUTO-REFRESH", "Refresh failed, returning error response");
|
|
return response; // Return original error
|
|
}
|
|
}
|
|
|
|
return response; // Return response (could be error)
|
|
} catch (error) {
|
|
if (attempt >= maxRetries) {
|
|
throw error;
|
|
}
|
|
|
|
log?.error?.(
|
|
"CLAUDE-WEB-AUTO-REFRESH",
|
|
`Fetch failed: ${error instanceof Error ? error.message : String(error)}`
|
|
);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
throw new Error("Max retries exceeded");
|
|
};
|
|
}
|