use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use serde::{Deserialize, Serialize};
use weave::lenses::{apply_stack, clarity::ClarityLens, narrow::NarrowLens, Lens, LensContext};
use crate::server::{routes::auth, AppState};
const LIMIT_ANON: i64 = 100;
const LIMIT_FREE: i64 = 500;
const LIMIT_BEACON: i64 = 2_000;
const LIMIT_STUDIO: i64 = 10_000;
const MAX_CONTENT_BYTES: usize = 2 * 1024 * 1024;
#[derive(Debug, Deserialize)]
pub struct CompressRequest {
pub content: String,
pub budget_tokens: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct CompressResponse {
pub compressed: String,
pub tokens_before: usize,
pub tokens_after: usize,
pub savings_pct: f64,
}
fn client_ip(headers: &HeaderMap) -> String {
headers
.get("x-forwarded-for")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.split(',').next())
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string())
}
pub async fn compress(
State(state): State<AppState>,
headers: HeaderMap,
Json(req): Json<CompressRequest>,
) -> impl IntoResponse {
if req.content.len() > MAX_CONTENT_BYTES {
return (
StatusCode::PAYLOAD_TOO_LARGE,
Json(serde_json::json!({
"error": "content exceeds 2 MB limit — install bctx CLI for local compression"
})),
)
.into_response();
}
let token = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(String::from);
let (rate_key, daily_limit, authenticated_uid) = match token {
Some(ref t) => match auth::verify_jwt(t, &state.jwt_secret) {
Some(uid) => {
let tier = state
.db
.get_user(&uid)
.map(|u| u.tier)
.unwrap_or_else(|| "free".to_string());
let limit = match tier.as_str() {
"enterprise" => i64::MAX,
"studio" => LIMIT_STUDIO,
"beacon" => LIMIT_BEACON,
_ => LIMIT_FREE,
};
(uid.clone(), limit, Some(uid))
}
None => {
let ip = format!("ip:{}", client_ip(&headers));
(ip, LIMIT_ANON, None)
}
},
None => {
let ip = format!("ip:{}", client_ip(&headers));
(ip, LIMIT_ANON, None)
}
};
if !state.db.check_compress_rate_limit(&rate_key, daily_limit) {
return (
StatusCode::TOO_MANY_REQUESTS,
Json(serde_json::json!({
"error": "daily limit reached",
"hint": "Sign in at betterctx.com for higher limits, or install the bctx CLI for unlimited local compression"
})),
)
.into_response();
}
let budget = req.budget_tokens.unwrap_or(2000);
let ctx = LensContext::new(budget);
let lenses: Vec<Box<dyn Lens>> = vec![Box::new(ClarityLens), Box::new(NarrowLens)];
let output = apply_stack(&lenses, &req.content, &ctx);
let tokens_saved = output.tokens_before.saturating_sub(output.tokens_after) as i64;
let record_key = authenticated_uid.as_deref().unwrap_or(&rate_key);
let _ = state.db.record_usage(
record_key,
output.tokens_before as i64,
tokens_saved,
"web_compress",
);
let savings_pct = output.savings_pct();
(
StatusCode::OK,
[("access-control-allow-origin", "*")],
Json(CompressResponse {
compressed: output.content,
tokens_before: output.tokens_before,
tokens_after: output.tokens_after,
savings_pct,
}),
)
.into_response()
}
pub async fn compress_options() -> impl IntoResponse {
(
StatusCode::NO_CONTENT,
[
("access-control-allow-origin", "*"),
("access-control-allow-methods", "POST, OPTIONS"),
(
"access-control-allow-headers",
"content-type, authorization",
),
("access-control-max-age", "86400"),
],
)
}