SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
3.2 KB · 76 lines typescript
Raw Blame History
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 * SOAX residential / mobile network. Gateway `proxy.soax.com:5000`.8 * Username grammar: `package-<id>[-country-xx][-region-xxx][-city-xxx][-sessionid-<id>][-sessionlength-<sec>]`.9 * Documentation: https://helpcenter.soax.com/10 *11 * The adapter is complete but the platform only routes to it once credentials are present12 * (`SOAX_USERNAME` / `SOAX_PASSWORD`). Until then `isConfigured()` is false and the admin13 * dashboard shows it as "unconfigured" — we never pretend a route exists.14 */15export class SoaxProvider implements ProxyProvider {16  readonly id = "soax" as const;17  readonly label = "SOAX";18  readonly networks: ConcreteNetwork[] = ["residential", "mobile"];19  private readonly username: string | undefined;20  private readonly password: string | undefined;21  private readonly prices: Record<string, number>;2223  constructor(env: NodeJS.ProcessEnv = process.env, prices: Record<string, number> = { residential: 9, mobile: 15 }) {24    this.username = env.SOAX_USERNAME?.trim() || undefined;25    this.password = env.SOAX_PASSWORD?.trim() || undefined;26    this.prices = prices;27  }2829  isConfigured(): boolean {30    return Boolean(this.username && this.password);31  }3233  supportsGeo(_geo: GeoTarget): boolean {34    return true;35  }3637  pricePerGb(network: ConcreteNetwork): number {38    return this.prices[network] ?? this.prices.residential ?? 9;39  }4041  estimateCost(network: ConcreteNetwork, bytes: number): number {42    return (bytes / 1_073_741_824) * this.pricePerGb(network);43  }4445  /** Build the upstream gateway endpoint for a request (exposed for contract tests; never returned to customers). */46  endpoint(req: Pick<ProviderRequest, "geo" | "sessionKey" | "sessionMinutes">): ProxyEndpoint {47    if (!this.username || !this.password) throw new Error("SOAX is not configured");48    let user = this.username;49    if (req.geo.country) user += `-country-${req.geo.country.toLowerCase()}`;50    if (req.geo.region) user += `-region-${req.geo.region}`;51    if (req.geo.city) user += `-city-${req.geo.city}`;52    if (req.sessionKey) {53      user += `-sessionid-${sanitize(req.sessionKey)}`;54      user += `-sessionlength-${Math.min(Math.max((req.sessionMinutes ?? 10) * 60, 60), 3600)}`;55    }56    return { host: "proxy.soax.com", port: 5000, username: user, password: this.password };57  }5859  proxyEndpoint(req: Pick<ProviderRequest, "geo" | "sessionKey" | "sessionMinutes">): ProxyEndpoint | null {60    return this.endpoint(req);61  }6263  async fetch(request: ProviderRequest): Promise<ProviderResponse> {64    return executeHttp(this.id, this.endpoint(request), request);65  }6667  async health(network: ConcreteNetwork = "residential"): Promise<ProviderHealth> {68    if (!this.isConfigured()) return { provider: this.id, network, status: "unconfigured", latencyMs: null, checkedAt: new Date(), detail: "Missing credentials" };69    return probeHealth(this, network, "https://checker.soax.com/api/ipinfo");70  }71}7273function sanitize(s: string): string {74  return s.replace(/[^a-zA-Z0-9]/g, "").slice(0, 32) || "s";75}76