import type { ConcreteNetwork, GeoTarget } from "@fetcha/core"; import { executeHttp } from "./http"; import type { ProviderHealth, ProviderRequest, ProviderResponse, ProxyEndpoint, ProxyProvider } from "./types"; import { probeHealth } from "./probe"; /** * Oxylabs residential network. Gateway `pr.oxylabs.io:7777`. * Username grammar: `customer-[-cc-XX][-st-][-city-][-sessid-][-sesstime-]`. * Documentation: https://developers.oxylabs.io/proxies/residential-proxies */ export class OxylabsProvider implements ProxyProvider { readonly id = "oxylabs" as const; readonly label = "Oxylabs"; readonly networks: ConcreteNetwork[] = ["residential"]; private readonly username: string | undefined; private readonly password: string | undefined; private readonly prices: Record; constructor(env: NodeJS.ProcessEnv = process.env, prices: Record = { residential: 8 }) { this.username = env.OXYLABS_USERNAME?.trim() || undefined; this.password = env.OXYLABS_PASSWORD?.trim() || undefined; this.prices = prices; } isConfigured(): boolean { return Boolean(this.username && this.password); } supportsGeo(_geo: GeoTarget): boolean { return true; // worldwide residential coverage } pricePerGb(network: ConcreteNetwork): number { return this.prices[network] ?? this.prices.residential ?? 8; } estimateCost(network: ConcreteNetwork, bytes: number): number { return (bytes / 1_073_741_824) * this.pricePerGb(network); } /** Build the upstream gateway endpoint for a request (exposed for contract tests; never returned to customers). */ endpoint(req: Pick): ProxyEndpoint { if (!this.username || !this.password) throw new Error("Oxylabs is not configured"); // Base username may already be `customer-xxx` or `customer-xxx-cc-US`; strip any geo suffix we control. let user = this.username.replace(/-cc-[a-z]{2}(-.*)?$/i, ""); if (!user.startsWith("customer-")) user = `customer-${user}`; if (req.geo.country) user += `-cc-${req.geo.country.toUpperCase()}`; if (req.geo.region && (req.geo.country === "US" || req.geo.country === "CA")) user += `-st-${req.geo.country.toLowerCase()}_${req.geo.region}`; if (req.geo.city) user += `-city-${req.geo.city}`; if (req.sessionKey) { user += `-sessid-${sanitize(req.sessionKey)}`; user += `-sesstime-${Math.min(Math.max(req.sessionMinutes ?? 10, 1), 30)}`; } return { host: "pr.oxylabs.io", port: 7777, username: user, password: this.password }; } proxyEndpoint(req: Pick): ProxyEndpoint | null { return this.endpoint(req); } async fetch(request: ProviderRequest): Promise { return executeHttp(this.id, this.endpoint(request), request); } async health(network: ConcreteNetwork = "residential"): Promise { if (!this.isConfigured()) return { provider: this.id, network, status: "unconfigured", latencyMs: null, checkedAt: new Date(), detail: "Missing credentials" }; return probeHealth(this, network, "https://ip.oxylabs.io/location"); } } function sanitize(s: string): string { return s.replace(/[^a-zA-Z0-9]/g, "").slice(0, 32) || "s"; }