Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
"description": "A browser automation client built on Playwright for Firefox",
"dependencies": {
"@supabase/supabase-js": "2.50.5",
"https-proxy-agent": "^7.0.6",
"playwright": "^1.56.1",
"uuid": "^11.1.0",
"ws": "^8.18.3"
},
"devDependencies": {
"@types/ws": "^8.18.1",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.18.1",
"typescript": "^5.8.3"
}
}
127 changes: 122 additions & 5 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,34 @@ import { ReplayManager } from "./replay-manager";
import { StorageManager } from "./storage-manager";
import { DataUsageTracker } from "./data-usage-tracker";
import { formatProxyURL } from "./utils";
import { IPGeolocationService, ProxyConfig } from "./ip-geolocation";

// macOS-compatible screen resolutions for realistic fingerprinting
const COMMON_SCREEN_RESOLUTIONS = [
{ width: 1920, height: 1080 }, // External monitor (very common)
{ width: 1440, height: 900 }, // 15" MacBook Pro default
{ width: 2560, height: 1440 }, // External monitor QHD
{ width: 1680, height: 1050 }, // Older MacBook Pro
{ width: 2560, height: 1600 }, // 16" MacBook Pro
{ width: 3024, height: 1964 }, // 14" MacBook Pro
{ width: 2880, height: 1800 }, // 15" Retina MacBook Pro
];

function generateScreenDimensions(): {
width: number;
height: number;
colorDepth: number;
} {
const resolution =
COMMON_SCREEN_RESOLUTIONS[
Math.floor(Math.random() * COMMON_SCREEN_RESOLUTIONS.length)
];
return {
width: resolution.width,
height: resolution.height,
colorDepth: 24,
};
}

export class RoverfoxClient {
private supabaseClient: SupabaseClient;
Expand All @@ -23,6 +51,7 @@ export class RoverfoxClient {
private replayManager: ReplayManager;
private storageManager: StorageManager;
private dataUsageTrackers: Map<string, DataUsageTracker>;
private geoService: IPGeolocationService;
private debug: boolean;

constructor(
Expand All @@ -39,6 +68,7 @@ export class RoverfoxClient {
this.replayManager = new ReplayManager(debug);
this.storageManager = new StorageManager(supabaseClient);
this.dataUsageTrackers = new Map();
this.geoService = new IPGeolocationService();

// Set up streaming message handler
this.connectionPool.setStreamingMessageHandler((message) => {
Expand Down Expand Up @@ -72,13 +102,15 @@ export class RoverfoxClient {
throw new Error("Profile not found");
}

// Fetch proxy data if needed
// Fetch proxy data from database
const { data: proxyId } = await this.supabaseClient
.from("accounts")
.select("proxyId")
.eq("browserId", browserId);

let proxyObject: RoverfoxProxyObject = null;
let proxyConfig: ProxyConfig | null = null;

if (proxyId) {
const { data: proxyData } = await this.supabaseClient
.from("proxies")
Expand All @@ -93,6 +125,44 @@ export class RoverfoxClient {
username,
password,
};
proxyConfig = { host: entry, port, username, password };
}
}

// Get current exit IP and check if geolocation needs updating
// This handles both hostname proxies (gw.dataimpulse.com) and IP rotation
if (proxyConfig) {
const { changed, currentIP } = await this.geoService.hasIPChanged(
proxyConfig,
profile.data.lastKnownIP || null,
);

if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;

// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);

if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
Comment on lines +140 to +163
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve the previous IP for accurate debug logs.

profile.data.lastKnownIP is overwritten before logging, so the “old → new” message becomes misleading.

💡 Suggested tweak
-        if (geoData) {
+        if (geoData) {
+          const previousIP = profile.data.lastKnownIP ?? null;
           profile.data.timezone = geoData.timezone;
           profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
           profile.data.countryCode = geoData.countryCode;
           profile.data.lastKnownIP = currentIP;
...
-              `IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
+              `IP changed for ${browserId}: ${previousIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;
// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);
if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${profile.data.lastKnownIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
if (currentIP && (changed || !profile.data.timezone)) {
const geoData = await this.geoService.lookup(currentIP);
if (geoData) {
const previousIP = profile.data.lastKnownIP ?? null;
profile.data.timezone = geoData.timezone;
profile.data.geolocation = { lat: geoData.lat, lon: geoData.lon };
profile.data.countryCode = geoData.countryCode;
profile.data.lastKnownIP = currentIP;
// Persist to database
await this.supabaseClient
.from("redrover_profile_data")
.update({ data: profile.data })
.eq("browser_id", browserId);
if (this.debug) {
if (changed) {
console.log(
`IP changed for ${browserId}: ${previousIP} -> ${currentIP}, updated geolocation to ${geoData.timezone}`,
);
} else {
console.log(
`Geolocation set for ${browserId}: ${geoData.timezone}`,
);
}
🤖 Prompt for AI Agents
In `@src/client.ts` around lines 113 - 136, Before overwriting
profile.data.lastKnownIP, save the existing value to a temp variable and use
that saved value in the debug log so the "old -> new" message is accurate;
specifically, in the block that handles currentIP changes (the code referencing
profile.data.lastKnownIP, currentIP, geoData and calling
this.supabaseClient.from("redrover_profile_data").update), capture previousIP =
profile.data.lastKnownIP before assigning profile.data.lastKnownIP = currentIP
and then use previousIP in the console.log when changed is true.

}
}
}
}

Expand Down Expand Up @@ -129,6 +199,8 @@ export class RoverfoxClient {
browser_id: browserId,
data: {
fontSpacingSeed: Math.floor(Math.random() * 100000000),
audioFingerprintSeed: Math.floor(Math.random() * 0xffffffff) + 1,
screenDimensions: generateScreenDimensions(),
storageState: {
cookies: [],
origins: [],
Expand Down Expand Up @@ -297,13 +369,16 @@ export class RoverfoxClient {
*/
async createProfile(
proxyUrl: string,
proxyState: string | null,
proxyId: number,
geoState: string | null = null,
): Promise<RoverFoxProfileData> {
let browserId = uuidv4();
let profile: RoverFoxProfileData = {
const browserId = uuidv4();
const profile: RoverFoxProfileData = {
browser_id: browserId,
data: {
fontSpacingSeed: Math.floor(Math.random() * 100000000),
audioFingerprintSeed: Math.floor(Math.random() * 0xffffffff) + 1,
screenDimensions: generateScreenDimensions(),
storageState: {
cookies: [],
origins: [],
Expand All @@ -312,10 +387,52 @@ export class RoverfoxClient {
},
};

// Track detected geo for accounts table
let detectedLat: number | null = null;
let detectedLon: number | null = null;

// Get proxy credentials to lookup geolocation via actual exit IP
const { data: proxyData } = await this.supabaseClient
.from("proxies")
.select("entry, port, username, password")
.eq("id", proxyId)
.single();

if (proxyData) {
const proxyConfig: ProxyConfig = {
host: proxyData.entry,
port: proxyData.port,
username: proxyData.username,
password: proxyData.password,
};

// Get real exit IP through the proxy
const result = await this.geoService.lookupThroughProxy(proxyConfig);
if (result) {
profile.data.timezone = result.geo.timezone;
profile.data.geolocation = { lat: result.geo.lat, lon: result.geo.lon };
profile.data.countryCode = result.geo.countryCode;
profile.data.lastKnownIP = result.ip;

// Store for accounts table
detectedLat = result.geo.lat;
detectedLon = result.geo.lon;

if (this.debug) {
console.log(
`Profile ${browserId} created with IP ${result.ip}, timezone ${result.geo.timezone}`,
);
}
}
}

await this.supabaseClient.from("accounts").insert({
browserId: browserId,
platform: "roverfox",
proxyState: proxyState,
proxyId: proxyId,
geo_state: geoState,
geo_latitude: detectedLat,
geo_longitude: detectedLon,
});

await this.supabaseClient.from("redrover_profile_data").insert(profile);
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ export type {

// Re-export utilities
export { formatProxyURL, createProxyUrl } from "./utils";

// Re-export geolocation service
export { IPGeolocationService, getGeoService } from "./ip-geolocation";
export type { GeoLocationData, ProxyConfig } from "./ip-geolocation";
Loading