import type { ConcreteNetwork, GeoTarget, HttpMethod } from "@fetcha/core"; import type { CookieJar } from "./cookies"; import type { FingerprintProfile } from "./fingerprint"; export type ProviderId = "oxylabs" | "decodo" | "soax" | "direct"; export interface ProxyEndpoint { host: string; port: number; username: string; password: string; } export interface ProviderRequest { requestId: string; attemptId: string; url: string; method: HttpMethod; headers: Record; body?: string | Buffer; timeoutMs: number; network: ConcreteNetwork; geo: GeoTarget; /** Sticky session key (provider-agnostic). When set the provider keeps the same exit IP. */ sessionKey?: string | null; /** Session lifetime hint in minutes. */ sessionMinutes?: number; followRedirects: boolean; maxRedirects: number; maxResponseBytes: number; /** Called for every redirect hop; must throw to abort. */ onRedirect?: (nextUrl: string) => Promise; /** Browser fingerprint profile to emulate (headers, TLS). Picked by the executor. */ profile?: FingerprintProfile; /** Accept-Language source. */ locale?: string | null; /** Referer to send on the first hop (null/undefined = none). */ referer?: string | null; /** Cookie jar shared across hops (and across requests for sticky sessions). */ jar?: CookieJar; /** Prefer HTTP/2 when the origin supports it (default true). */ http2?: boolean; } export interface ProviderTiming { dns_ms: number; proxy_connect_ms: number; tls_ms: number; origin_ms: number; processing_ms: number; total_ms: number; } export interface ProviderResponse { status: number; headers: Record; body: Buffer; finalUrl: string; redirects: number; bytesIn: number; bytesOut: number; timing: ProviderTiming; /** Provider exit info when known (never exposed to customers). */ exit?: { ip?: string; country?: string }; /** Fingerprint profile that was used. */ profileId?: string; /** Negotiated protocol of the final hop when known ("h2" | "http/1.1"). */ protocol?: string; } export type ProviderHealthStatus = "healthy" | "degraded" | "down" | "unconfigured"; export interface ProviderHealth { provider: ProviderId; network: ConcreteNetwork; status: ProviderHealthStatus; latencyMs: number | null; detail?: string; checkedAt: Date; } export class ProviderError extends Error { readonly provider: ProviderId; readonly kind: "timeout" | "connect" | "auth" | "proxy" | "too_large" | "redirect" | "tls" | "unknown"; readonly status?: number; constructor(provider: ProviderId, kind: ProviderError["kind"], message: string, opts: { status?: number; cause?: unknown } = {}) { super(message, { cause: opts.cause }); this.name = "ProviderError"; this.provider = provider; this.kind = kind; this.status = opts.status; } } export interface ProxyProvider { readonly id: ProviderId; /** Human label for admin screens. */ readonly label: string; /** Network classes this provider can serve. */ readonly networks: ConcreteNetwork[]; /** Whether credentials are present. Unconfigured providers are never routed to. */ isConfigured(): boolean; /** Countries the provider supports; null = worldwide/unknown. */ supportsGeo(geo: GeoTarget): boolean; fetch(request: ProviderRequest): Promise; health(network?: ConcreteNetwork): Promise; /** Estimated upstream cost in USD for a request transferring `bytes`. */ estimateCost(network: ConcreteNetwork, bytes: number): number; /** Unit price in USD per GB for a network class. */ pricePerGb(network: ConcreteNetwork): number; /** Upstream proxy endpoint for a request (null = direct egress). Used by the managed browser. */ proxyEndpoint(req: Pick): ProxyEndpoint | null; }