use appcore_contracts::ProviderConfig;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::time::Duration;
pub const GATEWAY_ADAPTER_NAME: &str = "gateway";
pub const GATEWAY_PROVIDER_ID: &str = "appcore-gateway";
pub const MAX_GATEWAY_MESSAGE_BYTES: usize = 4 * 1024 * 1024;
pub const MAX_GATEWAY_HTTP_BODY_BYTES: usize = MAX_GATEWAY_MESSAGE_BYTES * 4 + 65_536;
pub const MAX_GATEWAY_CAPABILITIES: usize = 64;
pub const MAX_GATEWAY_WORKERS_PER_TENANT: usize = 1_024;
pub const MAX_GATEWAY_CLIENTS_PER_TENANT: usize = 4_096;
pub const MAX_GATEWAY_CONNECTIONS: usize = 8_192;
pub const MAX_GATEWAY_PENDING_PER_TENANT: usize = 2_048;
pub const MAX_GATEWAY_WORKER_INFLIGHT: u64 = 64;
pub const MAX_GATEWAY_AFFINITY_KEY_BYTES: usize = 128;
pub const MAX_GATEWAY_TENANTS: usize = 1_024;
pub const MAX_GATEWAY_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GatewayConfig {
pub bind_address: SocketAddr,
pub domain_suffix: String,
require_auth: bool,
pub heartbeat_interval: Duration,
pub heartbeat_timeout: Duration,
}
impl GatewayConfig {
pub fn new(bind_address: SocketAddr, domain_suffix: impl Into<String>) -> Self {
Self {
bind_address,
domain_suffix: domain_suffix.into(),
require_auth: true,
heartbeat_interval: Duration::from_secs(30),
heartbeat_timeout: Duration::from_secs(90),
}
}
pub fn requires_authentication(&self) -> bool {
self.require_auth
}
pub fn from_provider_config(provider: &ProviderConfig) -> crate::GatewayResult<Self> {
validate_provider_shape(provider)?;
let settings = provider.settings();
let bind_address = required_setting(settings, "bind_address")?
.parse::<SocketAddr>()
.map_err(|error| {
crate::GatewayError::Config(format!("invalid bind_address: {error}"))
})?;
let domain_suffix = required_setting(settings, "domain_suffix")?.to_string();
let mut config = Self::new(bind_address, domain_suffix);
config.heartbeat_interval =
duration_setting(settings, "heartbeat_interval_ms", config.heartbeat_interval)?;
config.heartbeat_timeout =
duration_setting(settings, "heartbeat_timeout_ms", config.heartbeat_timeout)?;
config.validate()?;
Ok(config)
}
pub fn insecure_local_for_testing(mut self) -> Result<Self, crate::error::GatewayError> {
if !self.bind_address.ip().is_loopback() {
return Err(crate::error::GatewayError::Config(
"insecure gateway mode requires a loopback bind address".to_string(),
));
}
self.require_auth = false;
Ok(self)
}
pub fn validate(&self) -> Result<(), crate::error::GatewayError> {
if !valid_domain_suffix(&self.domain_suffix) {
return Err(crate::error::GatewayError::Config(
"domain_suffix must be an explicit valid DNS suffix".to_string(),
));
}
if self.heartbeat_interval.is_zero() {
return Err(crate::error::GatewayError::Config(
"heartbeat_interval must be greater than zero".to_string(),
));
}
if self.heartbeat_timeout <= self.heartbeat_interval {
return Err(crate::error::GatewayError::Config(
"heartbeat_timeout must be strictly greater than heartbeat_interval".to_string(),
));
}
if !self.require_auth && !self.bind_address.ip().is_loopback() {
return Err(crate::error::GatewayError::Config(
"gateway authentication cannot be disabled on a non-loopback bind address"
.to_string(),
));
}
Ok(())
}
}
fn validate_provider_shape(provider: &ProviderConfig) -> crate::GatewayResult<()> {
if provider.provider_id().as_str() != GATEWAY_PROVIDER_ID {
return Err(crate::GatewayError::Config(format!(
"gateway adapter requires provider_id={GATEWAY_PROVIDER_ID}"
)));
}
if provider.endpoint().is_some() {
return Err(crate::GatewayError::Config(
"gateway adapter does not accept a provider endpoint".to_string(),
));
}
if !provider.secret_refs().is_empty() {
return Err(crate::GatewayError::Config(
"gateway adapter reuses Runtime security and accepts no secret refs".to_string(),
));
}
const SETTINGS: [&str; 4] = [
"bind_address",
"domain_suffix",
"heartbeat_interval_ms",
"heartbeat_timeout_ms",
];
if let Some(name) = provider
.settings()
.keys()
.find(|name| !SETTINGS.contains(&name.as_str()))
{
return Err(crate::GatewayError::Config(format!(
"unsupported gateway setting: {name}"
)));
}
Ok(())
}
fn required_setting<'a>(
settings: &'a BTreeMap<String, String>,
name: &'static str,
) -> crate::GatewayResult<&'a str> {
settings
.get(name)
.map(String::as_str)
.filter(|value| !value.is_empty())
.ok_or_else(|| crate::GatewayError::Config(format!("gateway requires {name}")))
}
fn duration_setting(
settings: &BTreeMap<String, String>,
name: &'static str,
default: Duration,
) -> crate::GatewayResult<Duration> {
let Some(value) = settings.get(name) else {
return Ok(default);
};
value
.parse::<u64>()
.map(Duration::from_millis)
.map_err(|_| crate::GatewayError::Config(format!("{name} must be a u64")))
}
fn valid_domain_suffix(value: &str) -> bool {
if value.is_empty() || value.len() > 253 || value != value.trim() {
return false;
}
value.split('.').all(|label| {
!label.is_empty()
&& label.len() <= 63
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
})
}