use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
#[serde(default = "default_host")]
pub host: String,
pub port: u16,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LogConfig {
#[serde(default = "default_log_level")]
pub level: String,
#[serde(default)]
pub format: LogFormat,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StoreConfig {
pub data_dir: PathBuf,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct AuthConfig {
#[serde(default = "default_access_token_expiry")]
pub access_token_expiry: u64,
#[serde(default = "default_refresh_token_expiry")]
pub refresh_token_expiry: u64,
#[serde(default = "default_challenge_ttl")]
pub challenge_ttl: u64,
#[serde(default = "default_session_cleanup_interval")]
pub session_cleanup_interval: u64,
pub jwt_signing_key: Option<String>,
}
impl std::fmt::Debug for AuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthConfig")
.field("access_token_expiry", &self.access_token_expiry)
.field("refresh_token_expiry", &self.refresh_token_expiry)
.field("challenge_ttl", &self.challenge_ttl)
.field("session_cleanup_interval", &self.session_cleanup_interval)
.field(
"jwt_signing_key",
&self.jwt_signing_key.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MessagingConfig {
#[serde(default)]
pub mediator_url: String,
pub mediator_did: String,
#[serde(default)]
pub mediator_host: Option<String>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct SecretsConfig {
#[serde(alias = "seed", alias = "secret")]
pub inline_secret: Option<String>,
pub aws_secret_name: Option<String>,
pub aws_region: Option<String>,
pub gcp_project: Option<String>,
pub gcp_secret_name: Option<String>,
pub azure_vault_url: Option<String>,
pub azure_secret_name: Option<String>,
pub keyring_service: String,
}
impl std::fmt::Debug for SecretsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretsConfig")
.field(
"inline_secret",
&self.inline_secret.as_ref().map(|_| "<redacted>"),
)
.field("aws_secret_name", &self.aws_secret_name)
.field("aws_region", &self.aws_region)
.field("gcp_project", &self.gcp_project)
.field("gcp_secret_name", &self.gcp_secret_name)
.field("azure_vault_url", &self.azure_vault_url)
.field("azure_secret_name", &self.azure_secret_name)
.field("keyring_service", &self.keyring_service)
.finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditConfig {
#[serde(default = "default_audit_retention_days")]
pub retention_days: u32,
}
fn default_audit_retention_days() -> u32 {
28
}
impl Default for AuditConfig {
fn default() -> Self {
Self {
retention_days: default_audit_retention_days(),
}
}
}
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum LogFormat {
#[default]
Text,
Json,
}
fn default_host() -> String {
"0.0.0.0".to_string()
}
fn default_log_level() -> String {
"info".to_string()
}
fn default_access_token_expiry() -> u64 {
900
}
fn default_refresh_token_expiry() -> u64 {
86400
}
fn default_challenge_ttl() -> u64 {
300
}
fn default_session_cleanup_interval() -> u64 {
600
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
access_token_expiry: default_access_token_expiry(),
refresh_token_expiry: default_refresh_token_expiry(),
challenge_ttl: default_challenge_ttl(),
session_cleanup_interval: default_session_cleanup_interval(),
jwt_signing_key: None,
}
}
}
impl Default for LogConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: LogFormat::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_config_debug_redacts_jwt_signing_key() {
let cfg = AuthConfig {
access_token_expiry: 900,
refresh_token_expiry: 86400,
challenge_ttl: 300,
session_cleanup_interval: 600,
jwt_signing_key: Some("SUPER_SECRET_KEY_MATERIAL_MUST_NOT_LEAK".into()),
};
let dbg = format!("{cfg:?}");
assert!(
!dbg.contains("SUPER_SECRET_KEY_MATERIAL"),
"AuthConfig Debug leaked jwt_signing_key contents: {dbg}"
);
assert!(
dbg.contains("<redacted>"),
"expected redaction marker in Debug, got: {dbg}"
);
assert!(
dbg.contains("900"),
"access_token_expiry must still be visible: {dbg}"
);
}
#[test]
fn auth_config_debug_none_signing_key_renders_none() {
let cfg = AuthConfig::default();
let dbg = format!("{cfg:?}");
assert!(dbg.contains("jwt_signing_key: None"), "got: {dbg}");
}
#[test]
fn secrets_config_debug_redacts_inline_secret() {
let cfg = SecretsConfig {
inline_secret: Some("MASTER_SEED_HEX_MUST_NOT_LEAK".into()),
aws_secret_name: None,
aws_region: None,
gcp_project: None,
gcp_secret_name: None,
azure_vault_url: None,
azure_secret_name: None,
keyring_service: "vta".into(),
};
let dbg = format!("{cfg:?}");
assert!(
!dbg.contains("MASTER_SEED_HEX"),
"SecretsConfig Debug leaked inline_secret contents: {dbg}"
);
assert!(
dbg.contains("<redacted>"),
"expected redaction marker in Debug, got: {dbg}"
);
assert!(
dbg.contains("vta"),
"keyring_service must be visible: {dbg}"
);
}
#[test]
fn auth_config_serialize_still_carries_jwt_signing_key() {
let cfg = AuthConfig {
access_token_expiry: 900,
refresh_token_expiry: 86400,
challenge_ttl: 300,
session_cleanup_interval: 600,
jwt_signing_key: Some("key-material".into()),
};
let json = serde_json::to_string(&cfg).expect("serialize");
assert!(
json.contains("key-material"),
"Serialize must not redact — config persistence relies on round-trip: {json}"
);
}
}