import fs from "node:fs";
import path from "node:path";
import { OmniFlowSessionExportSchema, cloakFingerprintSeed, } from "dp-shared";
const OMNI_FLOW_SESSION_FILENAME = "omni-flow-session.json";
/** Read + parse the local养号 session blob, or null if missing/invalid. */
function readSessionBlob(config) {
    const blobPath = path.join(config.workspace, ".dipclaw", OMNI_FLOW_SESSION_FILENAME);
    if (!fs.existsSync(blobPath))
        return null;
    try {
        const parsed = OmniFlowSessionExportSchema.safeParse(JSON.parse(fs.readFileSync(blobPath, "utf8")));
        return parsed.success ? parsed.data : null;
    }
    catch {
        return null;
    }
}
/** Convert our structured config proxy into cloakbrowser's proxy option. */
function configProxyToCloak(proxy) {
    if (!proxy)
        return undefined;
    return {
        server: `${proxy.protocol}://${proxy.host}:${proxy.port}`,
        ...(proxy.username ? { username: proxy.username } : {}),
        ...(proxy.password ? { password: proxy.password } : {}),
    };
}
/**
 * Prefer the session blob's proxy + timezone (same source chrome mode applies
 * via OmniFlow, keeping cookies and exit-IP consistent). Fall back to the
 * center-bootstrapped config only when the blob carries no proxy.
 */
function resolveRouting(config, blob) {
    const pc = blob?.extensionStorage.proxyConfig;
    if (pc && pc.host && pc.port) {
        const timezone = blob.extensionStorage.timezoneEnabled === false
            ? undefined
            : blob.extensionStorage.spoofTimezone || undefined;
        return {
            proxy: {
                // OmniFlow stores `type` as 'socks5' | 'http' etc., port as string|number.
                server: `${String(pc.type).toLowerCase()}://${pc.host}:${pc.port}`,
                ...(pc.username ? { username: pc.username } : {}),
                ...(pc.password ? { password: pc.password } : {}),
            },
            timezone,
            source: "session-blob",
        };
    }
    const proxy = configProxyToCloak(config.browser.proxy);
    if (proxy) {
        return { proxy, timezone: config.browser.timezone, source: "config" };
    }
    return { timezone: config.browser.timezone, source: "none" };
}
/** Map a Chrome-exported cookie's sameSite to Playwright's enum. */
function mapSameSite(v) {
    switch ((v ?? "").toLowerCase()) {
        case "no_restriction":
        case "none":
            return "None";
        case "lax":
            return "Lax";
        case "strict":
            return "Strict";
        default:
            return undefined; // 'unspecified' / unknown -> let Playwright default
    }
}
/** Convert an OmniFlow-exported cookie to Playwright's addCookies shape. */
function toPlaywrightCookie(c) {
    if (!c.name || !c.domain)
        return null;
    const out = {
        name: c.name,
        value: c.value ?? "",
        domain: c.domain,
        path: c.path || "/",
    };
    if (typeof c.secure === "boolean")
        out.secure = c.secure;
    if (typeof c.httpOnly === "boolean")
        out.httpOnly = c.httpOnly;
    // Persistent cookies carry an absolute expiry (Unix seconds); session
    // cookies omit it so Playwright treats them as session-scoped.
    if (!c.session && typeof c.expirationDate === "number") {
        out.expires = Math.floor(c.expirationDate);
    }
    const sameSite = mapSameSite(c.sameSite);
    if (sameSite) {
        out.sameSite = sameSite;
        // Chrome/Playwright require SameSite=None cookies to be Secure.
        if (sameSite === "None")
            out.secure = true;
    }
    return out;
}
/**
 * Inject the养号 cookies into the freshly-launched context BEFORE any
 * navigation. Clears the persistent profile's existing cookies first (mirrors
 * the extension's clear→set semantics) so a stale profile can't leak old login
 * state. Resilient: a single malformed cookie falls back to per-cookie inserts
 * instead of dropping the whole set.
 */
async function injectCookies(ctx, cookies) {
    await ctx.clearCookies().catch(() => { });
    if (cookies.length === 0)
        return 0;
    try {
        await ctx.addCookies(cookies);
        return cookies.length;
    }
    catch {
        let n = 0;
        for (const c of cookies) {
            try {
                await ctx.addCookies([c]);
                n++;
            }
            catch {
                // Skip individual bad cookies, same tolerance as the extension.
            }
        }
        return n;
    }
}
/**
 * Stable, per-worker CloakBrowser fingerprint seed.
 *
 * CloakBrowser's `--fingerprint=<seed>` deterministically drives the WHOLE
 * fingerprint (GPU, hardware concurrency, device memory, screen, canvas/WebGL/
 * audio/font noise). Same seed = same consistent device identity across
 * sessions; different seed = a different but internally-consistent device.
 *
 * The package default randomizes the seed on EVERY launch — right for one-off
 * scraping, WRONG for养号: the same account would present a different device
 * fingerprint each login (a strong anomaly signal). For an account matrix we
 * want each worker to be ONE fixed, unique device: stable across that worker's
 * relaunches, distinct from other workers.
 *
 * We derive it deterministically from the worker name (stable + unique per
 * worker; survives restarts/redeploys/machine moves — unlike the workspace path
 * which is machine-specific). Mapped into CloakBrowser's 5-digit seed space
 * (10000-99999); note that space is only 90k wide, so very large matrices have
 * a small collision chance (an inherent CloakBrowser limit, same as its random
 * default).
 *
 * Keyed on `config.browser.fingerprintKey` (= the immutable worker_id in center
 * mode), so renames/redeploys keep the same device; falls back to the worker
 * name in standalone/direct mode. The seed math lives in dp-shared
 * (`cloakFingerprintSeed`) so the web admin can display the exact same value.
 */
/**
 * Best-effort IANA timezone -> BCP-47 locale. We feed our own per-proxy
 * timezone (no geoip), and a locale that roughly matches keeps the two
 * consistent for fingerprinting. Unknown zones fall back to en-US, which is
 * also CloakBrowser's default.
 */
function localeFromTimezone(timezone) {
    if (!timezone)
        return "en-US";
    const map = {
        "Asia/Shanghai": "zh-CN",
        "Asia/Hong_Kong": "zh-HK",
        "Asia/Taipei": "zh-TW",
        "Asia/Tokyo": "ja-JP",
        "Asia/Seoul": "ko-KR",
        "Asia/Singapore": "en-SG",
        "Europe/London": "en-GB",
        "Europe/Paris": "fr-FR",
        "Europe/Berlin": "de-DE",
        "Europe/Madrid": "es-ES",
        "Europe/Moscow": "ru-RU",
        "America/Sao_Paulo": "pt-BR",
    };
    if (map[timezone])
        return map[timezone];
    if (timezone.startsWith("America/"))
        return "en-US";
    if (timezone.startsWith("Australia/"))
        return "en-AU";
    return "en-US";
}
/**
 * Launch a CloakBrowser persistent context, inject养号 cookies, and return it.
 * Cookies are injected (awaited) before the context is handed back, so the
 * first task navigation already carries the login state — no race where a page
 * opens before cookies land.
 */
export async function launchCloakBrowserContext(params) {
    const { userDataDir, config } = params;
    // 养号: pin the binary version. CloakBrowser otherwise checks GitHub hourly
    // and silently swaps in a newer Chromium on the next launch — which can change
    // the seed→fingerprint mapping, i.e. an account's "device" shifts mid-operation.
    // Default auto-update OFF (set in code so every host gets it without per-host
    // env config); an explicit env value still wins. Upgrades become deliberate.
    if (process.env.CLOAKBROWSER_AUTO_UPDATE == null) {
        process.env.CLOAKBROWSER_AUTO_UPDATE = "false";
    }
    const blob = readSessionBlob(config);
    const routing = resolveRouting(config, blob);
    // Lazy import: keeps cloakbrowser (and its binary resolution) out of the
    // chrome-mode path entirely.
    const cloak = (await import("cloakbrowser"));
    // Pre-fetch the patched Chromium if not already cached on this host. No-op
    // (fast existsSync check) once present.
    await cloak.ensureBinary();
    // Stable per-worker fingerprint: pass our own --fingerprint=<seed>, which
    // overrides CloakBrowser's random default (buildArgs dedups by flag name, so
    // user args win). Each worker = one fixed, unique device across relaunches.
    // Anchor on the immutable worker_id (fingerprintKey) so a rename doesn't
    // change the device; fall back to the name in standalone/direct mode.
    const seed = cloakFingerprintSeed(config.browser.fingerprintKey ?? config.name);
    // No extensionPaths: this backend loads zero extensions by design.
    //
    // humanize: false — CloakBrowser's humanize monkey-patches page.mouse.move /
    // page.keyboard.type / page.click. Our own humanize.ts (used by actions.ts for
    // BOTH backends) already drives those with bezier curves + jittered timing, so
    // enabling CloakBrowser's would double-process every move/keystroke (our N-point
    // path → each point re-curved by CloakBrowser). We keep our humanize.ts as the
    // single, backend-agnostic behavior layer; only the binary-level fingerprint +
    // proxy come from CloakBrowser.
    const ctx = (await cloak.launchPersistentContext({
        userDataDir,
        headless: config.browser.headless,
        ...(routing.proxy ? { proxy: routing.proxy } : {}),
        ...(routing.timezone ? { timezone: routing.timezone } : {}),
        locale: localeFromTimezone(routing.timezone),
        args: [`--fingerprint=${seed}`],
        humanize: false,
    }));
    // Inject cookies before any navigation happens.
    const cookies = (blob?.cookies ?? [])
        .map(toPlaywrightCookie)
        .filter((c) => c !== null);
    const injected = await injectCookies(ctx, cookies);
    console.log(`🛡️  CloakBrowser launched (no extension) — fingerprint seed: ${seed} (stable per worker), ` +
        `proxy/timezone source: ${routing.source} ` +
        `(proxy: ${routing.proxy?.server ?? "none"}, timezone: ${routing.timezone ?? "system"}), ` +
        `cookies injected: ${injected}/${cookies.length}`);
    return ctx;
}
//# sourceMappingURL=cloakbrowser-backend.js.map