feat(emex): Floxy sticky sessions (5min) to hold one IP across a chained flow

emex's ssd tokens appear IP-bound: a rotating proxy sends each hop of a
decode→tree→drill→parts flow from a different exit IP, so emex rejects the
ssd and returns empty (the rotating-Floxy verify still seeded 0). Pin the
Floxy exit IP with a sticky session (password suffix
`_session-<id>_lifetime-300`); a rolling session id is reused across the
flow and rotates near expiry or after a transport failure (dead IP → fresh
one). lifetime via EMEX_FLOXY_LIFETIME (0 = rotating).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 16:00:53 +03:00
parent ca9b11dff7
commit 435250d6de

View File

@@ -116,13 +116,20 @@ export class EmexService {
portEnd: number;
} | null;
// Floxy residential fallback — used when the DataImpulse pool throws transport
// errors. Single rotating endpoint (one port; IP rotates per request).
// errors. `lifetime` > 0 → sticky session (one exit IP held that many seconds,
// appended to the password as `_session-<id>_lifetime-<n>`); 0 → rotating.
private readonly emexFloxy: {
host: string;
port: number;
user: string;
pass: string;
lifetime: number;
} | null;
// Rolling Floxy sticky-session id: held for ~lifetime so a chained emex flow
// (decode → tree → drill → parts) keeps ONE exit IP and emex's IP-bound ssd
// tokens stay valid. Rotated when stale or after a transport failure.
private floxySessionId = Math.random().toString(36).slice(2, 10);
private floxySessionBornMs = Date.now();
private readonly emexDirectFallback: boolean;
constructor(
@@ -176,13 +183,17 @@ export class EmexService {
const floxyEnabled = this.configService.get<string>("EMEX_FLOXY_FALLBACK", "true") === "true";
if (floxyEnabled) {
const fport = Number(this.configService.get("EMEX_FLOXY_PORT", 12321));
const flife = Number(this.configService.get("EMEX_FLOXY_LIFETIME", 300));
this.emexFloxy = {
host: this.configService.get<string>("EMEX_FLOXY_HOST", "residential.floxy.io"),
port: Number.isInteger(fport) && fport >= 1 && fport <= 65535 ? fport : 12321,
user: this.configService.get<string>("EMEX_FLOXY_USER", "d739255e819b"),
pass: this.configService.get<string>("EMEX_FLOXY_PASS", "9092873ba4e0"),
lifetime: Number.isInteger(flife) && flife >= 0 ? flife : 300,
};
this.logger.log(`EMEX Floxy fallback enabled: ${this.emexFloxy.host}:${this.emexFloxy.port}`);
this.logger.log(
`EMEX Floxy fallback enabled: ${this.emexFloxy.host}:${this.emexFloxy.port} (${this.emexFloxy.lifetime > 0 ? `sticky ${this.emexFloxy.lifetime}s` : "rotating"})`,
);
} else {
this.emexFloxy = null;
}
@@ -293,12 +304,32 @@ export class EmexService {
* without requiring authentication cookies.
*/
/** Build a fresh proxy agent on a random port from the pool (null if proxy off). */
/** Fresh Floxy sticky-session id (new exit IP). */
private rotateFloxySession(): void {
this.floxySessionId = Math.random().toString(36).slice(2, 10);
this.floxySessionBornMs = Date.now();
}
/**
* Floxy auth password. With lifetime > 0, appends `_session-<id>_lifetime-<n>`
* to pin one exit IP across a chained emex flow; the session id rolls over
* once it nears the lifetime so we never reuse an expired sticky slot.
*/
private floxyPassword(): string {
if (!this.emexFloxy) return "";
const { pass, lifetime } = this.emexFloxy;
if (lifetime <= 0) return pass; // rotating mode
// Roll at ~80% of lifetime so a sticky IP is never reused past expiry.
if (Date.now() - this.floxySessionBornMs > lifetime * 800) this.rotateFloxySession();
return `${pass}_session-${this.floxySessionId}_lifetime-${lifetime}`;
}
private newProxyAgent(provider: "dataimpulse" | "floxy" = "dataimpulse"): ProxyAgent | null {
if (provider === "floxy") {
if (!this.emexFloxy) return null;
const { host, port, user, pass } = this.emexFloxy;
const { host, port, user } = this.emexFloxy;
return new ProxyAgent({
uri: `http://${user}:${pass}@${host}:${port}`,
uri: `http://${user}:${this.floxyPassword()}@${host}:${port}`,
connect: { timeout: 30000 },
requestTls: { timeout: 30000 },
});
@@ -354,6 +385,9 @@ export class EmexService {
`${e.message} ${String(e.cause ?? "")}`,
));
if (!transient || attempt === maxAttempts) break;
// A Floxy transport failure means the current sticky exit IP is dead —
// roll to a fresh session so the next Floxy attempt gets a new IP.
if (provider === "floxy") this.rotateFloxySession();
const next = schedule[attempt];
const via =
next === "floxy" && provider !== "floxy" ? "via Floxy fallback" : "with fresh proxy";