feat(facebook): add challenge detection and session warming utilities

facebook-challenge.ts: session warmup, header construction, and challenge type detection. Spec document for the anti-bot challenge solver design.
This commit is contained in:
2026-05-02 19:03:00 -04:00
parent 0a246a29bf
commit ec545723bb
2 changed files with 301 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
// Facebook Marketplace session & challenge utilities
// ------------------ Types ------------------
export type ChallengeType =
| "login_wall"
| "checkpoint"
| "bad_headers"
| "rate_limited"
| "none";
// ------------------ Constants ------------------
const FACEBOOK_BROWSER_HEADERS: Record<string, string> = {
accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
"cache-control": "no-cache",
"upgrade-insecure-requests": "1",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"sec-ch-ua":
'"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Linux"',
"user-agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
};
// ------------------ Cookie Management ------------------
function parseSetCookies(setCookieHeaders: string[]): Record<string, string> {
const cookies: Record<string, string> = {};
for (const header of setCookieHeaders) {
const parts = header.split(";");
const firstPart = parts[0]?.trim();
if (!firstPart) continue;
const eqIdx = firstPart.indexOf("=");
if (eqIdx === -1) continue;
const name = firstPart.slice(0, eqIdx).trim();
const value = firstPart.slice(eqIdx + 1).trim();
if (name && value) {
cookies[name] = value;
}
}
return cookies;
}
function cookiesToHeader(cookies: Record<string, string>): string {
return Object.entries(cookies)
.map(([name, value]) => `${name}=${value}`)
.join("; ");
}
// ------------------ Session Warmup ------------------
export async function warmFacebookSession(): Promise<Record<string, string>> {
try {
const res = await fetch("https://www.facebook.com/", {
method: "GET",
headers: FACEBOOK_BROWSER_HEADERS,
redirect: "manual",
signal: AbortSignal.timeout(10000),
});
const setCookies = res.headers.getSetCookie?.() ?? [];
return parseSetCookies(setCookies);
} catch {
return {};
}
}
// ------------------ Challenge Detection ------------------
export function detectFacebookChallenge(
status: number,
html: string,
responseUrl: string,
): ChallengeType {
if (status === 400) {
return "bad_headers";
}
if (status === 429) {
return "rate_limited";
}
if (responseUrl.includes("/login/")) {
return "login_wall";
}
if (html.includes("You must log in") || html.includes("log in to continue")) {
return "login_wall";
}
if (
responseUrl.includes("/checkpoint/") ||
(html.includes("checkpoint") && html.includes("challenge"))
) {
return "checkpoint";
}
return "none";
}
// ------------------ Header Construction ------------------
export function buildFacebookHeaders(
cookieJar: Record<string, string>,
extraHeaders?: Record<string, string>,
): Record<string, string> {
const headers: Record<string, string> = {
...FACEBOOK_BROWSER_HEADERS,
};
const cookieString = cookiesToHeader(cookieJar);
if (cookieString) {
headers.cookie = cookieString;
}
if (extraHeaders) {
Object.assign(headers, extraHeaders);
}
return headers;
}