use axum::http::request::Parts;
use super::{PreparedProxyRequest, ProxyPrimitive, prepare};
use crate::admitted::{self, Admission, CallerAdmission, GatewayRefusal, Payload};
use crate::routing::RouteDecision;
use lean_ctx::core::context_admission::egress::{self, EgressTarget};
pub(super) fn trusted_destination_binding<'a>(
parts: &'a Parts,
route: Option<&'a RouteDecision>,
) -> egress::TrustedDestinationBinding<'a> {
let caller_scope_override = [
"openai-organization",
"openai-project",
"chatgpt-account-id",
]
.iter()
.any(|header| parts.headers.contains_key(*header))
|| crate::providers::has_untrusted_key_query(&parts.uri);
let signed_region = parts
.extensions
.get::<crate::bedrock::SigningContext>()
.map(|context| context.region.as_str());
if let Some(route) =
route.filter(|route| route.provider_id.is_some() || route.upstream_base.is_some())
{
return egress::TrustedDestinationBinding {
provider_id: route.provider_id.as_deref(),
local: route.local,
account_ref: (route.gateway_managed_credential && !caller_scope_override)
.then_some(route.account_ref.as_deref())
.flatten(),
region: signed_region.or(route.region.as_deref()),
};
}
parts
.extensions
.get::<crate::providers::RegistryProviderId>()
.map_or_else(
|| egress::TrustedDestinationBinding {
region: signed_region,
..Default::default()
},
|provider| egress::TrustedDestinationBinding {
provider_id: Some(&provider.id),
local: Some(provider.local),
account_ref: (provider.gateway_managed_credential && !caller_scope_override)
.then_some(provider.account_ref.as_deref())
.flatten(),
region: signed_region.or(provider.region.as_deref()),
},
)
}
fn payload(parsed: Option<&serde_json::Value>, opaque_bytes: usize) -> Payload<'_> {
parsed.map_or(Payload::Opaque(opaque_bytes), Payload::Document)
}
fn model_of(parsed: Option<&serde_json::Value>) -> Option<&str> {
parsed
.and_then(|doc| doc.get("model"))
.and_then(serde_json::Value::as_str)
}
#[derive(Clone, Copy)]
pub(super) struct Leaving<'a> {
pub(super) provider: &'a str,
pub(super) upstream_base: &'a str,
pub(super) route: Option<&'a RouteDecision>,
}
pub(crate) fn sealed_principal(parts: &Parts) -> Option<Caller> {
#[cfg(feature = "enterprise")]
let person = parts
.extensions
.get::<crate::gateway_identity::GatewayTags>()
.and_then(|tags| tags.person.as_deref());
#[cfg(not(feature = "enterprise"))]
let person = None;
principal_of(&parts.headers, person)
}
pub(crate) fn principal_of(
headers: &axum::http::HeaderMap,
person: Option<&str>,
) -> Option<Caller> {
let mut identity = blake3::Hasher::new();
let mut known = false;
if let Some(person) = person {
identity.update(b"person\0");
identity.update(person.as_bytes());
known = true;
}
for header in ["authorization", "x-api-key", "x-goog-api-key"] {
if let Some(value) = headers.get(header) {
identity.update(header.as_bytes());
identity.update(b"\0");
identity.update(value.as_bytes());
known = true;
}
}
known.then(|| Caller {
receipt: identity.finalize().to_hex().to_string(),
person: person.map(|person| {
let bound = format!("person\0{person}");
lean_ctx::core::context_admission::receipt_store::keyed::principal_ref(&bound)
.unwrap_or_else(|_| blake3::hash(bound.as_bytes()).to_hex().to_string())
}),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Caller {
pub(crate) receipt: String,
pub(crate) person: Option<String>,
}
pub(super) fn admit_caller(
parts: &Parts,
parsed: Option<&serde_json::Value>,
raw_bytes: usize,
leaving: Leaving<'_>,
) -> Result<CallerAdmission, GatewayRefusal> {
let target = EgressTarget {
provider: leaving.provider,
model: leaving
.route
.map(|route| route.model.as_str())
.or_else(|| model_of(parsed)),
upstream_base: leaving
.route
.and_then(|route| route.upstream_base.as_deref())
.unwrap_or(leaving.upstream_base),
};
admitted::admit_caller(
payload(parsed, raw_bytes),
&target,
trusted_destination_binding(parts, leaving.route),
sealed_principal(parts),
)
}
pub(super) struct FinalAdmission {
pub(super) body: Vec<u8>,
pub(super) admission: Admission,
}
pub(super) fn admit_final(
parts: &Parts,
body: Vec<u8>,
parsed: Option<&serde_json::Value>,
leaving: Leaving<'_>,
caller: CallerAdmission,
untouched: bool,
restore: impl FnOnce(Option<&serde_json::Value>) -> Option<serde_json::Value>,
) -> Result<FinalAdmission, GatewayRefusal> {
let target = EgressTarget {
provider: leaving.provider,
model: model_of(parsed),
upstream_base: leaving.upstream_base,
};
let (admission, rewritten) = admitted::admit_final_with(
payload(parsed, body.len()),
&target,
trusted_destination_binding(parts, leaving.route),
caller,
untouched,
restore,
)?;
let body = match rewritten {
Some(doc) => serde_json::to_vec(&doc)
.ok()
.and_then(|logical| prepare::encode_request_body(parts, logical).ok())
.ok_or_else(|| GatewayRefusal::unavailable("egress.encode_failed"))?,
None => body,
};
Ok(FinalAdmission { body, admission })
}
pub(super) async fn admit_target(
parts: &Parts,
parsed: Option<&serde_json::Value>,
leaving: Leaving<'_>,
) -> Result<(), GatewayRefusal> {
let target = EgressTarget {
provider: leaving.provider,
model: leaving
.route
.map(|route| route.model.as_str())
.or_else(|| model_of(parsed)),
upstream_base: leaving
.route
.and_then(|route| route.upstream_base.as_deref())
.unwrap_or(leaving.upstream_base),
};
admitted::admit_request_target(
parts
.uri
.path_and_query()
.map_or("/", axum::http::uri::PathAndQuery::as_str),
parts
.headers
.iter()
.filter(|(name, _)| super::headers::should_forward_request_header(name.as_str(), true)),
&target,
trusted_destination_binding(parts, leaving.route),
sealed_principal(parts).as_ref(),
)
.await
}
pub(super) fn refused(
refusal: &GatewayRefusal,
provider_label: &str,
original_bytes: usize,
trace_id: &str,
) -> PreparedProxyRequest {
PreparedProxyRequest::Terminal(Box::new(ProxyPrimitive::Policy {
response: refusal.response(provider_label),
original_tokens: original_bytes / 4,
trace_id: trace_id.to_owned(),
reason: GATEWAY_REFUSED,
}))
}
pub(super) const GATEWAY_REFUSED: &str = "context gateway refused request";