Merge PR 3000 into release/v3.8.8

This commit is contained in:
diegosouzapw
2026-05-31 18:09:18 -03:00
4 changed files with 154 additions and 263 deletions

227
package-lock.json generated
View File

@@ -63,7 +63,6 @@
"pino-abstract-transport": "^3.0.0",
"pino-pretty": "^13.1.3",
"playwright": "1.60.0",
"proxifly": "^3.0.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-is": "^19.2.6",
@@ -7727,6 +7726,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@@ -7955,6 +7955,7 @@
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -8768,6 +8769,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
@@ -11449,22 +11451,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/fs-jetpack": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-4.3.1.tgz",
"integrity": "sha512-dbeOK84F6BiQzk2yqqCVwCPWTxAvVGJ3fMQc6E2wuEohS28mR6yHngbrKuVCK1KHRx/ccByDylqu4H5PCP2urQ==",
"license": "MIT",
"dependencies": {
"minimatch": "^3.0.2",
"rimraf": "^2.6.3"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
@@ -12635,17 +12621,6 @@
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -13767,58 +13742,6 @@
"node": ">= 0.4"
}
},
"node_modules/itwcw-package-analytics": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/itwcw-package-analytics/-/itwcw-package-analytics-1.0.8.tgz",
"integrity": "sha512-Uvu8uj2iDlAAKkugGSJ0WD1dSZW7wec8Vso8qvcfKfmtHz/QGqITdu8TPsle5FuNnIm9fr/H3vjM1YfzLj37jA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"fs-jetpack": "^4.3.1",
"uuid": "^9.0.1",
"wonderful-fetch": "^1.3.4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/itwcw-package-analytics/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/itwcw-package-analytics/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/itwcw-package-analytics/node_modules/wonderful-fetch": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/wonderful-fetch/-/wonderful-fetch-1.3.4.tgz",
"integrity": "sha512-vyo+dH0e7kqFBd6hHuU+3VO8+N6iQ87gLrg6cwdj63VaXyXBsZKiSO6CoWeKzK4eRvdlUMB4hnsfNXK+sYdGgQ==",
"license": "MIT",
"dependencies": {
"fs-jetpack": "^4.3.1",
"itwcw-package-analytics": "^1.0.6",
"json5": "^2.2.1",
"mime-types": "^2.1.35",
"node-fetch": "^2.7.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/jiti": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
@@ -14025,6 +13948,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -16063,6 +15987,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -16422,48 +16347,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-fetch/node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/node-fetch/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/node-fetch/node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/node-loader": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/node-loader/-/node-loader-2.1.0.tgz",
@@ -17022,15 +16905,6 @@
"node": ">=14.0.0"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -17487,19 +17361,6 @@
"node": ">=12.0.0"
}
},
"node_modules/proxifly": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/proxifly/-/proxifly-3.0.1.tgz",
"integrity": "sha512-tb3fnZLF/H4d2jQr6sO9NVRqTXmJgNZtb3cEuR7bTEqiGDx6weaFZqXSxaxZQYA+KsE4u2OW+nPchvZ3rK9FVg==",
"license": "MIT",
"dependencies": {
"itwcw-package-analytics": "^1.0.8",
"wonderful-fetch": "^2.0.4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -18344,40 +18205,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/roarr": {
"version": "2.15.4",
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
@@ -21275,50 +21102,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wonderful-fetch": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/wonderful-fetch/-/wonderful-fetch-2.0.5.tgz",
"integrity": "sha512-V8uDL2A+ZDxVA5s11QsOGBaMgr4i9v7kM4yacyThyrKvnj0VpWqMPxxZLZ3/UkH+5/jq9XyK0oMXyZpRT1E8LQ==",
"license": "MIT",
"dependencies": {
"fs-jetpack": "^5.1.0",
"itwcw-package-analytics": "^1.0.8",
"mime-types": "^3.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/wonderful-fetch/node_modules/brace-expansion": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
"integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/wonderful-fetch/node_modules/fs-jetpack": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-5.1.0.tgz",
"integrity": "sha512-Xn4fDhLydXkuzepZVsr02jakLlmoARPy+YWIclo4kh0GyNGUHnTqeH/w/qIsVn50dFxtp8otPL2t/HcPJBbxUA==",
"license": "MIT",
"dependencies": {
"minimatch": "^5.1.0"
}
},
"node_modules/wonderful-fetch/node_modules/minimatch": {
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@@ -188,7 +188,6 @@
"pino-abstract-transport": "^3.0.0",
"pino-pretty": "^13.1.3",
"playwright": "1.60.0",
"proxifly": "^3.0.1",
"react": "19.2.6",
"react-dom": "19.2.6",
"react-is": "^19.2.6",

View File

@@ -3,16 +3,53 @@ import { isPrivateHost } from "@/shared/network/outboundUrlGuard";
const DEFAULT_QUANTITY = 100;
const DEFAULT_ANONYMITY = "elite";
const DEFAULT_API_URL = "https://api.proxifly.dev/proxy";
const DEFAULT_PROTOCOL = "http";
const DEFAULT_TIMEOUT_MS = 15_000;
const MAX_BATCH_QUANTITY = 20;
type ProxiflyProxy = {
ip: string;
port: number;
protocol: string;
country: string;
anonymity: string;
speed: number;
ip?: string;
port?: number | string;
protocol?: string;
country?: string;
anonymity?: string;
speed?: number;
score?: number;
quality_score?: number;
geolocation?: {
country?: string | null;
} | null;
};
function parsePositiveInt(value: string | undefined, fallback: number): number {
const parsed = parseInt(value || "", 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function normalizeProxyResponse(value: unknown): ProxiflyProxy[] {
if (Array.isArray(value)) return value as ProxiflyProxy[];
if (value && typeof value === "object") {
const maybeWrapped = value as { proxies?: unknown };
if (Array.isArray(maybeWrapped.proxies)) return maybeWrapped.proxies as ProxiflyProxy[];
return [value as ProxiflyProxy];
}
return [];
}
function normalizeProxyType(protocol: string | undefined): FreeProxyItem["type"] {
const normalized = protocol?.toLowerCase();
return normalized === "https" || normalized === "socks4" || normalized === "socks5"
? normalized
: "http";
}
function normalizeQualityScore(proxy: ProxiflyProxy): number | null {
const raw = proxy.speed ?? proxy.quality_score ?? proxy.score;
if (raw == null || !Number.isFinite(raw)) return null;
return Math.min(100, Math.max(0, Math.round(raw)));
}
export class ProxiflyProvider implements FreeProxyProvider {
readonly id = "proxifly" as const;
readonly name = "Proxifly";
@@ -27,50 +64,65 @@ export class ProxiflyProvider implements FreeProxyProvider {
}
const { upsertFreeProxy } = await import("../db/freeProxies");
const quantity =
parseInt(process.env.FREE_PROXY_PROXIFLY_QUANTITY || "", 10) || DEFAULT_QUANTITY;
const quantity = parsePositiveInt(process.env.FREE_PROXY_PROXIFLY_QUANTITY, DEFAULT_QUANTITY);
const anonymity = process.env.FREE_PROXY_PROXIFLY_ANONYMITY || DEFAULT_ANONYMITY;
const errors: string[] = [];
let added = 0;
let updated = 0;
let fetched = 0;
let requested = 0;
try {
const proxiflyModule = await import("proxifly");
const proxifly = proxiflyModule.default ?? proxiflyModule;
const result = await (proxifly as { getProxy: (opts: unknown) => Promise<unknown> }).getProxy(
{
protocol: "http",
anonymity: anonymity as "elite" | "anonymous" | "transparent",
speed: "fast",
quantity,
}
);
while (requested < quantity) {
const batchQuantity = Math.min(MAX_BATCH_QUANTITY, quantity - requested);
const url = new URL(DEFAULT_API_URL);
url.searchParams.set("format", "json");
url.searchParams.set("quantity", String(batchQuantity));
url.searchParams.set("protocol", DEFAULT_PROTOCOL);
url.searchParams.set("anonymity", anonymity);
const proxies: ProxiflyProxy[] = Array.isArray(result) ? result : [result as ProxiflyProxy];
const res = await fetch(url, {
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
});
for (const p of proxies) {
if (!p.ip || !p.port) continue;
if (isPrivateHost(p.ip)) {
errors.push(`Proxifly: skipped private/loopback host ${p.ip}`);
continue;
if (!res.ok) {
const text = await res.text().catch(() => "");
errors.push(`HTTP ${res.status}: ${text.slice(0, 100)}`);
break;
}
const item: FreeProxyItem = {
source: "proxifly",
host: p.ip,
port: Number(p.port),
type: (p.protocol || "http").toLowerCase() as FreeProxyItem["type"],
countryCode: p.country?.slice(0, 2).toUpperCase() || null,
qualityScore: p.speed != null ? Math.min(100, Math.max(0, Math.round(p.speed))) : null,
latencyMs: null,
anonymity: p.anonymity || null,
lastValidated: new Date().toISOString(),
};
const r = await upsertFreeProxy(item);
if (r.action === "created") added++;
else updated++;
fetched++;
const proxies = normalizeProxyResponse(await res.json());
if (proxies.length === 0) break;
requested += proxies.length;
for (const p of proxies) {
if (!p || !p.ip || !p.port) continue;
if (isPrivateHost(p.ip)) {
errors.push(`Proxifly: skipped private/loopback host ${p.ip}`);
continue;
}
const item: FreeProxyItem = {
source: "proxifly",
host: p.ip,
port: Number(p.port),
type: normalizeProxyType(p.protocol),
countryCode:
p.geolocation?.country?.slice(0, 2).toUpperCase() ||
p.country?.slice(0, 2).toUpperCase() ||
null,
qualityScore: normalizeQualityScore(p),
latencyMs: null,
anonymity: p.anonymity || null,
lastValidated: new Date().toISOString(),
};
const r = await upsertFreeProxy(item);
if (r.action === "created") added++;
else updated++;
fetched++;
}
if (proxies.length < batchQuantity) break;
}
} catch (err) {
errors.push(err instanceof Error ? err.message : String(err));

View File

@@ -45,7 +45,7 @@ test("getProvider returns the correct provider by id", () => {
});
test("getProvider returns undefined for unknown id", () => {
const p = getProvider("unknown" as any);
const p = getProvider("unknown" as Parameters<typeof getProvider>[0]);
assert.equal(p, undefined);
});
@@ -154,6 +154,63 @@ test("ProxiflyProvider.sync returns disabled error when not enabled", async () =
process.env.FREE_PROXY_PROXIFLY_ENABLED = original ?? "";
});
test("ProxiflyProvider.sync fetches proxies in API-sized batches", async () => {
await reset();
const originalEnabled = process.env.FREE_PROXY_PROXIFLY_ENABLED;
const originalQuantity = process.env.FREE_PROXY_PROXIFLY_QUANTITY;
const originalFetch = globalThis.fetch;
const requestedQuantities: string[] = [];
process.env.FREE_PROXY_PROXIFLY_ENABLED = "true";
process.env.FREE_PROXY_PROXIFLY_QUANTITY = "25";
globalThis.fetch = (async (input: RequestInfo | URL, _init?: RequestInit) => {
const url = new URL(String(input));
requestedQuantities.push(url.searchParams.get("quantity") || "");
assert.equal(url.searchParams.get("format"), "json");
assert.equal(url.searchParams.get("protocol"), "http");
assert.equal(url.searchParams.get("anonymity"), "elite");
const quantity = Number(url.searchParams.get("quantity"));
const batchIndex = requestedQuantities.length - 1;
const body = Array.from({ length: quantity }, (_, index) => ({
ip: `42.${batchIndex}.${index + 1}.1`,
port: 8000 + index,
protocol: "http",
anonymity: "elite",
score: 50 + index,
geolocation: { country: "US" },
}));
if (batchIndex === 0) {
body[0] = null as unknown as (typeof body)[number];
}
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;
try {
const p = getProvider("proxifly")!;
const result = await p.sync();
assert.deepEqual(requestedQuantities, ["20", "5"]);
assert.equal(result.fetched, 24);
assert.equal(result.added, 24);
assert.equal(result.updated, 0);
assert.deepEqual(result.errors, []);
const items = await p.list({ limit: 30 });
assert.equal(items.length, 24);
assert.ok(items.every((item) => item.source === "proxifly"));
} finally {
globalThis.fetch = originalFetch;
process.env.FREE_PROXY_PROXIFLY_ENABLED = originalEnabled ?? "";
process.env.FREE_PROXY_PROXIFLY_QUANTITY = originalQuantity ?? "";
}
});
// ── IplocateProvider ──────────────────────────────────────────────────────────
test("IplocateProvider.isEnabled returns false by default", () => {