appcore_gateway/
config.rs1use appcore_contracts::ProviderConfig;
14use std::collections::BTreeMap;
15use std::net::SocketAddr;
16use std::time::Duration;
17
18pub const GATEWAY_ADAPTER_NAME: &str = "gateway";
20pub const GATEWAY_PROVIDER_ID: &str = "appcore-gateway";
22
23pub const MAX_GATEWAY_MESSAGE_BYTES: usize = 4 * 1024 * 1024;
25pub const MAX_GATEWAY_HTTP_BODY_BYTES: usize = MAX_GATEWAY_MESSAGE_BYTES * 4 + 65_536;
27pub const MAX_GATEWAY_CAPABILITIES: usize = 64;
29pub const MAX_GATEWAY_WORKERS_PER_TENANT: usize = 1_024;
31pub const MAX_GATEWAY_CLIENTS_PER_TENANT: usize = 4_096;
33pub const MAX_GATEWAY_CONNECTIONS: usize = 8_192;
35pub const MAX_GATEWAY_PENDING_PER_TENANT: usize = 2_048;
37pub const MAX_GATEWAY_TENANTS: usize = 1_024;
39pub const MAX_GATEWAY_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
41
42#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct GatewayConfig {
48 pub bind_address: SocketAddr,
50
51 pub domain_suffix: String,
59
60 require_auth: bool,
62
63 pub heartbeat_interval: Duration,
65
66 pub heartbeat_timeout: Duration,
68}
69
70impl GatewayConfig {
71 pub fn new(bind_address: SocketAddr, domain_suffix: impl Into<String>) -> Self {
73 Self {
74 bind_address,
75 domain_suffix: domain_suffix.into(),
76 require_auth: true,
77 heartbeat_interval: Duration::from_secs(30),
78 heartbeat_timeout: Duration::from_secs(90),
79 }
80 }
81
82 pub fn requires_authentication(&self) -> bool {
84 self.require_auth
85 }
86
87 pub fn from_provider_config(provider: &ProviderConfig) -> crate::GatewayResult<Self> {
95 validate_provider_shape(provider)?;
96 let settings = provider.settings();
97 let bind_address = required_setting(settings, "bind_address")?
98 .parse::<SocketAddr>()
99 .map_err(|error| {
100 crate::GatewayError::Config(format!("invalid bind_address: {error}"))
101 })?;
102 let domain_suffix = required_setting(settings, "domain_suffix")?.to_string();
103 let mut config = Self::new(bind_address, domain_suffix);
104 config.heartbeat_interval =
105 duration_setting(settings, "heartbeat_interval_ms", config.heartbeat_interval)?;
106 config.heartbeat_timeout =
107 duration_setting(settings, "heartbeat_timeout_ms", config.heartbeat_timeout)?;
108 config.validate()?;
109 Ok(config)
110 }
111
112 pub fn insecure_local_for_testing(mut self) -> Result<Self, crate::error::GatewayError> {
114 if !self.bind_address.ip().is_loopback() {
115 return Err(crate::error::GatewayError::Config(
116 "insecure gateway mode requires a loopback bind address".to_string(),
117 ));
118 }
119 self.require_auth = false;
120 Ok(self)
121 }
122
123 pub fn validate(&self) -> Result<(), crate::error::GatewayError> {
125 if !valid_domain_suffix(&self.domain_suffix) {
126 return Err(crate::error::GatewayError::Config(
127 "domain_suffix must be an explicit valid DNS suffix".to_string(),
128 ));
129 }
130 if self.heartbeat_interval.is_zero() {
131 return Err(crate::error::GatewayError::Config(
132 "heartbeat_interval must be greater than zero".to_string(),
133 ));
134 }
135 if self.heartbeat_timeout <= self.heartbeat_interval {
136 return Err(crate::error::GatewayError::Config(
137 "heartbeat_timeout must be strictly greater than heartbeat_interval".to_string(),
138 ));
139 }
140 if !self.require_auth && !self.bind_address.ip().is_loopback() {
141 return Err(crate::error::GatewayError::Config(
142 "gateway authentication cannot be disabled on a non-loopback bind address"
143 .to_string(),
144 ));
145 }
146 Ok(())
147 }
148}
149
150fn validate_provider_shape(provider: &ProviderConfig) -> crate::GatewayResult<()> {
151 if provider.provider_id().as_str() != GATEWAY_PROVIDER_ID {
152 return Err(crate::GatewayError::Config(format!(
153 "gateway adapter requires provider_id={GATEWAY_PROVIDER_ID}"
154 )));
155 }
156 if provider.endpoint().is_some() {
157 return Err(crate::GatewayError::Config(
158 "gateway adapter does not accept a provider endpoint".to_string(),
159 ));
160 }
161 if !provider.secret_refs().is_empty() {
162 return Err(crate::GatewayError::Config(
163 "gateway adapter reuses Runtime security and accepts no secret refs".to_string(),
164 ));
165 }
166 const SETTINGS: [&str; 4] = [
167 "bind_address",
168 "domain_suffix",
169 "heartbeat_interval_ms",
170 "heartbeat_timeout_ms",
171 ];
172 if let Some(name) = provider
173 .settings()
174 .keys()
175 .find(|name| !SETTINGS.contains(&name.as_str()))
176 {
177 return Err(crate::GatewayError::Config(format!(
178 "unsupported gateway setting: {name}"
179 )));
180 }
181 Ok(())
182}
183
184fn required_setting<'a>(
185 settings: &'a BTreeMap<String, String>,
186 name: &'static str,
187) -> crate::GatewayResult<&'a str> {
188 settings
189 .get(name)
190 .map(String::as_str)
191 .filter(|value| !value.is_empty())
192 .ok_or_else(|| crate::GatewayError::Config(format!("gateway requires {name}")))
193}
194
195fn duration_setting(
196 settings: &BTreeMap<String, String>,
197 name: &'static str,
198 default: Duration,
199) -> crate::GatewayResult<Duration> {
200 let Some(value) = settings.get(name) else {
201 return Ok(default);
202 };
203 value
204 .parse::<u64>()
205 .map(Duration::from_millis)
206 .map_err(|_| crate::GatewayError::Config(format!("{name} must be a u64")))
207}
208
209fn valid_domain_suffix(value: &str) -> bool {
210 if value.is_empty() || value.len() > 253 || value != value.trim() {
211 return false;
212 }
213 value.split('.').all(|label| {
214 !label.is_empty()
215 && label.len() <= 63
216 && !label.starts_with('-')
217 && !label.ends_with('-')
218 && label
219 .bytes()
220 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
221 })
222}