tail-fin-common 0.1.0

Shared infrastructure for tail-fin: error types, page_fetch, cookies, CDP helpers
Documentation
use night_fury_core::BrowserSession;
use serde_json::Value;

use crate::error::TailFinError;

/// Ensure the browser is on one of the specified domains (e.g. "x.com", "grok.com").
///
/// If the current URL doesn't contain any of the domains, navigates to `https://{domains[0]}`
/// and waits for the network to settle.
pub async fn ensure_on_domain(session: &BrowserSession, domains: &[&str]) -> Result<(), TailFinError> {
    let current_url = session.get_url().await?;
    if !domains.iter().any(|d| current_url.contains(d)) {
        session.navigate(&format!("https://{}", domains[0])).await?;
        let _ = session.wait_for_network_idle(15000, 1000).await;
    }
    Ok(())
}

/// Execute a fetch request within the browser's page context.
///
/// This runs `fetch()` inside the current page, which means:
/// - Same-origin policy is satisfied
/// - Cookies are automatically included via `credentials: "include"`
/// - The request appears as a normal frontend request
pub async fn page_fetch(
    session: &BrowserSession,
    url: &str,
    method: &str,
    headers: &Value,
) -> Result<Value, TailFinError> {
    page_fetch_with_body(session, url, method, headers, None).await
}

/// Execute a fetch request with an optional JSON body.
pub async fn page_fetch_with_body(
    session: &BrowserSession,
    url: &str,
    method: &str,
    headers: &Value,
    body: Option<&Value>,
) -> Result<Value, TailFinError> {
    let url_escaped = serde_json::to_string(url).unwrap_or_default();
    let method_escaped = serde_json::to_string(method).unwrap_or_default();
    let headers_json = serde_json::to_string(headers).unwrap_or_default();
    let body_part = match body {
        Some(b) => {
            let body_json = serde_json::to_string(b).unwrap_or_default();
            format!(r#", body: JSON.stringify({})"#, body_json)
        }
        None => String::new(),
    };

    let js = format!(
        r#"
        (async () => {{
            const resp = await fetch({url_escaped}, {{
                method: {method_escaped},
                headers: {headers_json},
                credentials: "include"{body_part}
            }});
            if (!resp.ok) {{
                return {{ __error: true, status: resp.status, statusText: resp.statusText }};
            }}
            return await resp.json();
        }})()
        "#
    );

    let result = session.eval(&js).await?;

    if result
        .get("__error")
        .and_then(|v| v.as_bool())
        .unwrap_or(false)
    {
        let status = result.get("status").and_then(|v| v.as_u64()).unwrap_or(0);
        let text = result
            .get("statusText")
            .and_then(|v| v.as_str())
            .unwrap_or("unknown");
        return Err(TailFinError::Api(format!("HTTP {} {}", status, text)));
    }

    Ok(result)
}