Hmbown/CodeWhale · error

Browser login is only enabled for the canonical Codewhale…

Error message

Browser login is only enabled for the canonical Codewhale account API or a loopback test API

What it means

Raised by `validate_verification_url` in crates/cli/src/cloud.rs:1078 when the configured Codewhale account API base is neither the canonical `https://api.codewhale.net:443` nor a loopback host. Browser-based device login is hard-disabled for any other API endpoint, so the login flow refuses before validating the verification URL's origin.

Solutions

  1. Unset the custom API base (remove CODEWHALE_API_BASE / reset config) so login uses the canonical https://api.codewhale.net
  2. If testing locally, change the API base to a loopback address such as http://127.0.0.1:8080, which the login flow explicitly supports
  3. For a remote staging environment, use a non-browser login path (e.g. paste an API key) instead of the device login flow
  4. Verify the api_base string parses to scheme https, host api.codewhale.net, default port 443

Example fix

// before
export CODEWHALE_API_BASE=https://api.staging.example.com
codewhale login
// after
unset CODEWHALE_API_BASE
codewhale login
Defensive patterns

Strategy: validation

Validate before calling

fn api_base_allows_browser_login(api_base: &str) -> bool {
    url::Url::parse(api_base).map(|api| {
        let canonical = api.scheme() == "https"
            && api.host_str() == Some("api.codewhale.net")
            && api.port_or_known_default() == Some(443);
        let loopback = api.host_str()
            .map(|h| h.eq_ignore_ascii_case("localhost")
                || h.parse::<std::net::IpAddr>().is_ok_and(|a| a.is_loopback()))
            .unwrap_or(false);
        canonical || loopback
    }).unwrap_or(false)
}

Type guard

fn is_canonical_or_loopback_api(api_base: &str) -> bool {
    api_base_allows_browser_login(api_base)
}

Try / catch

match run_device_login() {
    Err(e) if e.to_string().contains("only enabled for the canonical") => {
        eprintln!("Browser login disabled for this API base; use an API key or switch to api.codewhale.net / localhost.");
    }
    other => return other,
}

Prevention

When it happens

Trigger: Running `codewhale login` (device/browser flow) with CODEWHALE_API_BASE (or config api_base) set to a remote non-canonical host, e.g. `https://api.staging.example.com` or an internal corporate gateway — anything that is not api.codewhale.net on 443 and not localhost/127.x/::1.

Common situations: Developers pointing the CLI at a staging or self-hosted Codewhale API and attempting interactive browser login; a proxy URL typo; environment variable left over from an earlier test session.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/06855256eea4df4e. Report an issue: GitHub.

Appendix: source

Thrown at crates/cli/src/cloud.rs:1078

    let api = Url::parse(api_base).context("invalid Codewhale account API base URL")?;
    let canonical_api = api.scheme() == "https"
        && api.host_str() == Some("api.codewhale.net")
        && api.port_or_known_default() == Some(443);
    let loopback_api = api.host_str().is_some_and(is_loopback_host);
    if canonical_api {
        if url.scheme() != "https"
            || !host.eq_ignore_ascii_case("app.codewhale.net")
            || url.port_or_known_default() != Some(443)
        {
            bail!("The Codewhale service returned an untrusted verification origin");
        }
    } else if loopback_api {
        if !matches!(url.scheme(), "http" | "https") || !is_loopback_host(host) {
            bail!("The Codewhale service returned an untrusted verification origin");
        }
    } else {
        bail!(
            "Browser login is only enabled for the canonical Codewhale account API or a loopback test API"
        );
    }

    let query = url.query_pairs().collect::<Vec<_>>();
    if complete {
        if query.len() != 1 || query[0].0 != "user_code" || query[0].1 != user_code {
            bail!("The Codewhale service returned an unsafe verification URL");
        }
    } else if !query.is_empty() {
        bail!("The Codewhale service returned an unsafe verification URL");
    }
    Ok(url.to_string())
}

fn is_loopback_host(host: &str) -> bool {
    let host = host
        .strip_prefix('[')

View on GitHub (pinned to 73e0f67d83)