import type { FreeProxyItem, FreeProxySyncResult, FreeProxyProvider } from "./types"; import { isPrivateHost } from "@/shared/network/outboundUrlGuard"; const DEFAULT_API_URL = "https://1proxy-api.aitradepulse.com/api/v1/proxies/advanced"; const DEFAULT_MAX = 500; const DEFAULT_MIN_QUALITY = 50; const DEFAULT_PAGE_SIZE = 100; const MAX_CONSECUTIVE_FAILURES = 5; type OneProxyApiProxy = { ip: string; port: number; protocol: string; country_code: string; latency_ms: number; anonymity: string; quality_score: number; last_validated: string; }; type OneProxyApiResponse = { total: number; count: number; offset: number; proxies: OneProxyApiProxy[]; }; export class OneproxyProvider implements FreeProxyProvider { readonly id = "1proxy" as const; readonly name = "1proxy"; private consecutiveFailures = 0; isEnabled(): boolean { return process.env.FREE_PROXY_1PROXY_ENABLED !== "false"; } private getConfig() { return { apiUrl: process.env.FREE_PROXY_1PROXY_API_URL || DEFAULT_API_URL, maxProxies: parseInt(process.env.FREE_PROXY_1PROXY_MAX || "", 10) || DEFAULT_MAX, minQuality: parseInt(process.env.FREE_PROXY_1PROXY_MIN_QUALITY || "", 10) || DEFAULT_MIN_QUALITY, }; } async sync(): Promise { if (!this.isEnabled()) { return { fetched: 0, added: 0, updated: 0, errors: ["1proxy provider disabled"] }; } if (this.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { return { fetched: 0, added: 0, updated: 0, errors: [`Circuit breaker open: ${this.consecutiveFailures} consecutive failures`], }; } const { upsertFreeProxy } = await import("../db/freeProxies"); const { apiUrl, maxProxies, minQuality } = this.getConfig(); const errors: string[] = []; let added = 0; let updated = 0; let fetched = 0; let offset = 0; try { while (fetched < maxProxies) { const limit = Math.min(DEFAULT_PAGE_SIZE, maxProxies - fetched); const url = `${apiUrl}?offset=${offset}&limit=${limit}&min_quality_score=${minQuality}`; const res = await fetch(url, { signal: AbortSignal.timeout(15000) }); if (!res.ok) { const text = await res.text().catch(() => ""); errors.push(`HTTP ${res.status}: ${text.slice(0, 100)}`); this.consecutiveFailures++; break; } const json = (await res.json()) as OneProxyApiResponse; if (!Array.isArray(json.proxies) || json.proxies.length === 0) break; for (const p of json.proxies) { if (!p.ip || isPrivateHost(p.ip)) { errors.push(`1proxy: skipped private/loopback host ${p.ip}`); continue; } const item: FreeProxyItem = { source: "1proxy", host: p.ip, port: p.port, type: (p.protocol?.toLowerCase() as FreeProxyItem["type"]) || "http", countryCode: p.country_code || null, qualityScore: p.quality_score ?? null, latencyMs: p.latency_ms ?? null, anonymity: p.anonymity || null, lastValidated: p.last_validated || new Date().toISOString(), }; const result = await upsertFreeProxy(item); if (result.action === "created") added++; else updated++; } fetched += json.proxies.length; offset += json.proxies.length; if (json.proxies.length < limit) break; } this.consecutiveFailures = 0; } catch (err) { this.consecutiveFailures++; errors.push(err instanceof Error ? err.message : String(err)); } return { fetched, added, updated, errors }; } async list(filters: { protocol?: string; country?: string; minQuality?: number; limit?: number; }): Promise { const { listFreeProxiesBySource } = await import("../db/freeProxies"); return listFreeProxiesBySource("1proxy", filters); } }