fix(preflight): Apply stored fingerprint to task browser

- Add WorkerFingerprint interface with timezone, city, state, ip, locale
- Store fingerprint in TaskWorker after preflight passes
- Pass fingerprint through TaskContext to handlers
- Apply timezone via CDP and locale via Accept-Language header
- Ensures browser fingerprint matches proxy IP location

This fixes anti-detect detection where timezone/locale mismatch
with proxy IP was getting blocked by Cloudflare.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-13 16:40:52 -07:00
parent 5ea92e25af
commit 023cfc127f
22 changed files with 3083 additions and 5 deletions

View File

@@ -0,0 +1,224 @@
/**
* Explore Jane API to understand data structure
* Usage: npx ts-node scripts/test-jane-api-explore.ts
*/
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
puppeteer.use(StealthPlugin());
async function main() {
console.log('Exploring Jane API from browser context...\n');
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
const page = await browser.newPage();
// Intercept network requests to find store data API calls
const capturedResponses: Array<{ url: string; data: any }> = [];
await page.setRequestInterception(true);
page.on('request', (req) => req.continue());
page.on('response', async (response) => {
const url = response.url();
if (url.includes('iheartjane.com') &&
(url.includes('/stores') || url.includes('/search') || url.includes('algolia'))) {
try {
const text = await response.text();
if (text.startsWith('{') || text.startsWith('[')) {
const data = JSON.parse(text);
capturedResponses.push({ url, data });
console.log(`Captured: ${url.substring(0, 100)}...`);
}
} catch {
// Not JSON
}
}
});
// Visit Jane to establish session
console.log('Visiting Jane stores page to capture network requests...');
await page.goto('https://www.iheartjane.com/stores', {
waitUntil: 'networkidle2',
timeout: 60000,
});
console.log(`\nCaptured ${capturedResponses.length} API responses`);
for (const resp of capturedResponses) {
console.log(`\n--- ${resp.url.substring(0, 80)} ---`);
const keys = Object.keys(resp.data);
console.log('Keys:', keys);
// Check for stores array
if (resp.data.stores && Array.isArray(resp.data.stores)) {
console.log(`Stores count: ${resp.data.stores.length}`);
const firstStore = resp.data.stores[0];
if (firstStore) {
console.log('First store keys:', Object.keys(firstStore));
console.log('Sample:', JSON.stringify(firstStore, null, 2).substring(0, 500));
}
}
// Check for hits (Algolia)
if (resp.data.hits && Array.isArray(resp.data.hits)) {
console.log(`Hits count: ${resp.data.hits.length}`);
const firstHit = resp.data.hits[0];
if (firstHit) {
console.log('First hit keys:', Object.keys(firstHit));
}
}
}
// Look for __NEXT_DATA__ or similar embedded data
console.log('\n--- Checking for embedded page data ---');
const pageData = await page.evaluate(() => {
// Check for Next.js data
const nextData = (window as any).__NEXT_DATA__;
if (nextData?.props?.pageProps?.stores) {
return {
source: '__NEXT_DATA__',
storeCount: nextData.props.pageProps.stores.length,
firstStore: nextData.props.pageProps.stores[0],
};
}
// Check for any global store data
const win = window as any;
if (win.stores) return { source: 'window.stores', data: win.stores };
if (win.__stores) return { source: 'window.__stores', data: win.__stores };
return null;
});
if (pageData) {
console.log('Found embedded data:', pageData.source);
console.log('Store count:', pageData.storeCount);
if (pageData.firstStore) {
console.log('First store keys:', Object.keys(pageData.firstStore));
console.log('Sample:', JSON.stringify({
id: pageData.firstStore.id,
name: pageData.firstStore.name,
city: pageData.firstStore.city,
state: pageData.firstStore.state,
}, null, 2));
}
} else {
console.log('No embedded page data found');
}
// Try alternative API endpoints from browser context
console.log('\n--- Testing alternative API endpoints ---');
// Try the map endpoint
const mapData = await page.evaluate(async () => {
try {
const res = await fetch('https://api.iheartjane.com/v1/stores/map?per_page=100');
if (res.ok) return await res.json();
} catch {}
return null;
});
if (mapData) {
console.log('\n/v1/stores/map response:');
console.log('Keys:', Object.keys(mapData));
if (mapData.stores?.[0]) {
console.log('First store keys:', Object.keys(mapData.stores[0]));
}
}
// Try index endpoint
const indexData = await page.evaluate(async () => {
try {
const res = await fetch('https://api.iheartjane.com/v1/stores/index?per_page=10');
if (res.ok) return await res.json();
} catch {}
return null;
});
if (indexData) {
console.log('\n/v1/stores/index response:');
console.log('Keys:', Object.keys(indexData));
if (indexData.stores?.[0]) {
console.log('First store keys:', Object.keys(indexData.stores[0]));
}
}
// Try with state parameter
const stateData = await page.evaluate(async () => {
try {
const res = await fetch('https://api.iheartjane.com/v1/stores?state=AZ&per_page=10');
if (res.ok) return await res.json();
} catch {}
return null;
});
if (stateData) {
console.log('\n/v1/stores?state=AZ response:');
console.log('Keys:', Object.keys(stateData));
console.log('Stores count:', stateData.stores?.length);
if (stateData.stores?.[0]) {
console.log('First store keys:', Object.keys(stateData.stores[0]));
console.log('Sample:', JSON.stringify(stateData.stores[0], null, 2).substring(0, 300));
}
}
// Try Algolia directly for stores
console.log('\n--- Testing Algolia for stores ---');
const algoliaStores = await page.evaluate(async () => {
try {
// Common Algolia search pattern
const res = await fetch('https://search.iheartjane.com/1/indexes/stores-production/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Algolia-Application-Id': 'HKXSXRD7RA',
'X-Algolia-API-Key': 'YjZhYjQxZjU4ZTNjMTRhYzExZTk2YjU2MzliMGE4ZTE5YjJkMmZkZTI2ODllYTY2MThlMzQ3Y2QxOTFkMjI5Y3RhZ0ZpbHRlcnM9',
},
body: JSON.stringify({
query: 'Arizona',
hitsPerPage: 20,
}),
});
if (res.ok) return await res.json();
} catch {}
return null;
});
if (algoliaStores) {
console.log('Algolia stores-production response:');
console.log('Keys:', Object.keys(algoliaStores));
console.log('Hits count:', algoliaStores.hits?.length);
if (algoliaStores.hits?.[0]) {
console.log('First hit keys:', Object.keys(algoliaStores.hits[0]));
console.log('Sample:', JSON.stringify(algoliaStores.hits[0], null, 2).substring(0, 500));
}
}
// Check if there's a /v2 endpoint
const v2Data = await page.evaluate(async () => {
try {
const res = await fetch('https://api.iheartjane.com/v2/stores?per_page=10');
if (res.ok) return await res.json();
} catch {}
return null;
});
if (v2Data) {
console.log('\n/v2/stores response:');
console.log('Keys:', Object.keys(v2Data));
if (v2Data.stores?.[0]) {
console.log('First store keys:', Object.keys(v2Data.stores[0]));
}
}
await browser.close();
console.log('\nDone!');
}
main().catch(console.error);