openlatch-client 0.1.18

OpenLatch runtime enforcement node — the capture-and-enforce client for the AI Operations Platform
//! Billing-mode detection (D-11).
//!
//! Inspect the caller's auth shape on the request — we already read the headers
//! to forward them, so this adds no work. **Default to `unknown`, never guess**
//! (D-20): an `unknown` suppresses dollar figures downstream (E-7) rather than
//! mispricing a call.
//!
//! ⚠️ **Unverified split (I-1 OQ3).** The `x-api-key` vs OAuth-`Bearer` mapping
//! has not been confirmed against a real Claude.ai Pro/Max subscription session.
//! When unsure the honest answer is `unknown`; track how often it fires (it
//! silently empties the money view — a logged risk).

use axum::http::header::{HeaderMap, AUTHORIZATION};

/// Frozen enum `billing_mode = api_key | subscription | unknown`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BillingMode {
    /// `x-api-key` present → pay-as-you-go API key billing.
    ApiKey,
    /// `Authorization: Bearer …` (OAuth) → a claude.ai subscription session.
    Subscription,
    /// Neither shape recognised — never guessed.
    Unknown,
}

impl BillingMode {
    /// Frozen wire string.
    pub fn as_str(&self) -> &'static str {
        match self {
            BillingMode::ApiKey => "api_key",
            BillingMode::Subscription => "subscription",
            BillingMode::Unknown => "unknown",
        }
    }
}

/// Classify the caller's billing mode from the request headers alone.
pub fn detect_billing(headers: &HeaderMap) -> BillingMode {
    if headers.contains_key("x-api-key") {
        BillingMode::ApiKey
    } else if is_oauth_bearer(headers) {
        BillingMode::Subscription
    } else {
        BillingMode::Unknown
    }
}

/// True when `Authorization` carries an OAuth `Bearer <token>` (subscription
/// shape). Case-insensitive on the scheme; a bare/empty value is not a match.
fn is_oauth_bearer(headers: &HeaderMap) -> bool {
    headers
        .get(AUTHORIZATION)
        .and_then(|v| v.to_str().ok())
        .map(|v| {
            let t = v.trim_start();
            t.len() > "bearer ".len() && t[.."bearer".len()].eq_ignore_ascii_case("bearer")
        })
        .unwrap_or(false)
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::http::{HeaderName, HeaderValue};

    fn hdr(pairs: &[(&str, &str)]) -> HeaderMap {
        let mut h = HeaderMap::new();
        for (k, v) in pairs {
            h.insert(
                HeaderName::from_bytes(k.as_bytes()).unwrap(),
                HeaderValue::from_str(v).unwrap(),
            );
        }
        h
    }

    #[test]
    fn x_api_key_is_api_key() {
        let h = hdr(&[("x-api-key", "sk-ant-xyz")]);
        assert_eq!(detect_billing(&h), BillingMode::ApiKey);
        assert_eq!(detect_billing(&h).as_str(), "api_key");
    }

    #[test]
    fn oauth_bearer_is_subscription() {
        let h = hdr(&[("authorization", "Bearer oauth-token-value")]);
        assert_eq!(detect_billing(&h), BillingMode::Subscription);
        assert_eq!(detect_billing(&h).as_str(), "subscription");
    }

    #[test]
    fn bearer_scheme_is_case_insensitive() {
        let h = hdr(&[("authorization", "bearer lower-scheme")]);
        assert_eq!(detect_billing(&h), BillingMode::Subscription);
    }

    #[test]
    fn no_recognised_shape_is_unknown() {
        assert_eq!(detect_billing(&HeaderMap::new()), BillingMode::Unknown);
        // A malformed/empty Authorization does not become subscription.
        let h = hdr(&[("authorization", "Bearer")]);
        assert_eq!(detect_billing(&h), BillingMode::Unknown);
    }

    #[test]
    fn x_api_key_wins_over_bearer() {
        // If both are present the pay-as-you-go key is authoritative.
        let h = hdr(&[("x-api-key", "sk-ant-xyz"), ("authorization", "Bearer t")]);
        assert_eq!(detect_billing(&h), BillingMode::ApiKey);
    }
}