TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import type { ConcreteNetwork, GeoTarget } from "@fetcha/core";2import { executeHttp } from "./http";3import type { ProviderHealth, ProviderRequest, ProviderResponse, ProxyEndpoint, ProxyProvider } from "./types";4import { probeHealth } from "./probe";56/**7 * Oxylabs residential network. Gateway `pr.oxylabs.io:7777`.8 * Username grammar: `customer-<user>[-cc-XX][-st-<state>][-city-<city>][-sessid-<id>][-sesstime-<min>]`.9 * Documentation: https://developers.oxylabs.io/proxies/residential-proxies10 */11export class OxylabsProvider implements ProxyProvider {12 readonly id = "oxylabs" as const;13 readonly label = "Oxylabs";14 readonly networks: ConcreteNetwork[] = ["residential"];15 private readonly username: string | undefined;16 private readonly password: string | undefined;17 private readonly prices: Record<string, number>;1819 constructor(env: NodeJS.ProcessEnv = process.env, prices: Record<string, number> = { residential: 8 }) {20 this.username = env.OXYLABS_USERNAME?.trim() || undefined;21 this.password = env.OXYLABS_PASSWORD?.trim() || undefined;22 this.prices = prices;23 }2425 isConfigured(): boolean {26 return Boolean(this.username && this.password);27 }2829 supportsGeo(_geo: GeoTarget): boolean {30 return true; // worldwide residential coverage31 }3233 pricePerGb(network: ConcreteNetwork): number {34 return this.prices[network] ?? this.prices.residential ?? 8;35 }3637 estimateCost(network: ConcreteNetwork, bytes: number): number {38 return (bytes / 1_073_741_824) * this.pricePerGb(network);39 }4041 /** Build the upstream gateway endpoint for a request (exposed for contract tests; never returned to customers). */42 endpoint(req: Pick<ProviderRequest, "geo" | "sessionKey" | "sessionMinutes">): ProxyEndpoint {43 if (!this.username || !this.password) throw new Error("Oxylabs is not configured");44 // Base username may already be `customer-xxx` or `customer-xxx-cc-US`; strip any geo suffix we control.45 let user = this.username.replace(/-cc-[a-z]{2}(-.*)?$/i, "");46 if (!user.startsWith("customer-")) user = `customer-${user}`;47 if (req.geo.country) user += `-cc-${req.geo.country.toUpperCase()}`;48 if (req.geo.region && (req.geo.country === "US" || req.geo.country === "CA")) user += `-st-${req.geo.country.toLowerCase()}_${req.geo.region}`;49 if (req.geo.city) user += `-city-${req.geo.city}`;50 if (req.sessionKey) {51 user += `-sessid-${sanitize(req.sessionKey)}`;52 user += `-sesstime-${Math.min(Math.max(req.sessionMinutes ?? 10, 1), 30)}`;53 }54 return { host: "pr.oxylabs.io", port: 7777, username: user, password: this.password };55 }5657 proxyEndpoint(req: Pick<ProviderRequest, "geo" | "sessionKey" | "sessionMinutes">): ProxyEndpoint | null {58 return this.endpoint(req);59 }6061 async fetch(request: ProviderRequest): Promise<ProviderResponse> {62 return executeHttp(this.id, this.endpoint(request), request);63 }6465 async health(network: ConcreteNetwork = "residential"): Promise<ProviderHealth> {66 if (!this.isConfigured()) return { provider: this.id, network, status: "unconfigured", latencyMs: null, checkedAt: new Date(), detail: "Missing credentials" };67 return probeHealth(this, network, "https://ip.oxylabs.io/location");68 }69}7071function sanitize(s: string): string {72 return s.replace(/[^a-zA-Z0-9]/g, "").slice(0, 32) || "s";73}74