diff --git a/crates/dtrack-config/src/lib.rs b/crates/dtrack-config/src/lib.rs new file mode 100644 index 0000000..f928de1 --- /dev/null +++ b/crates/dtrack-config/src/lib.rs @@ -0,0 +1,106 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +/// Laufzeit-Konfiguration des DT-MCP. +/// +/// Bewusst serde-(de)serialisierbar, damit sie spaeter ueber die Admin-UI +/// (Stage 3) editiert und auf Platte persistiert werden kann. Aufloesung: +/// explizite Datei (`DTRACK_CONFIG`) hat Vorrang, sonst Umgebungsvariablen. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DtrackConfig { + /// Basis-URL der DT-Instanz, z.B. https://dtrack.firma.intern + pub url: String, + /// API-Key eines read-only Teams. + pub api_key: String, + /// TLS-Zertpruefung deaktivieren (Notbehelf bei internem CA-Cert). + #[serde(default)] + pub insecure: bool, +} + +impl DtrackConfig { + /// Liest die Config aus einer TOML-Datei. + pub fn load(path: impl AsRef) -> Result { + let path = path.as_ref(); + let raw = std::fs::read_to_string(path) + .with_context(|| format!("Config nicht lesbar: {}", path.display()))?; + toml::from_str(&raw).context("Config-TOML ungueltig") + } + + /// Schreibt die Config als TOML. + pub fn save(&self, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let raw = toml::to_string_pretty(self).context("Config nicht serialisierbar")?; + std::fs::write(path, raw) + .with_context(|| format!("Config nicht schreibbar: {}", path.display())) + } + + /// Baut die Config aus Umgebungsvariablen (`DTRACK_URL` / `DTRACK_API_KEY` + /// / `DTRACK_INSECURE`). Das ist der Pfad fuers stdio-Binary auf dem Laptop. + pub fn from_env() -> Result { + let url = std::env::var("DTRACK_URL").context("DTRACK_URL nicht gesetzt")?; + let api_key = std::env::var("DTRACK_API_KEY").context("DTRACK_API_KEY nicht gesetzt")?; + let insecure = std::env::var("DTRACK_INSECURE") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false); + Ok(Self { url, api_key, insecure }) + } + + /// Aufloesung mit Vorrang: explizite Datei (`DTRACK_CONFIG`) -> sonst env. + pub fn resolve() -> Result { + match std::env::var("DTRACK_CONFIG") { + Ok(path) => Self::load(path), + Err(_) => Self::from_env(), + } + } + + /// Maskierte Darstellung des API-Keys fuer Logs/UI. + pub fn masked_api_key(&self) -> String { + mask_secret(&self.api_key) + } +} + +/// Maskiert ein Secret: alles ausser den letzten 4 Zeichen wird zu Punkten. +/// Reveal passiert spaeter explizit in der UI, nie by default. +pub fn mask_secret(secret: &str) -> String { + let n = secret.chars().count(); + if n <= 4 { + return "\u{2022}".repeat(n); + } + let visible: String = secret.chars().skip(n - 4).collect(); + format!("{}{visible}", "\u{2022}".repeat(n - 4)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mask_hides_all_but_last_four() { + let masked = mask_secret("odt_secret1234"); + assert!(masked.ends_with("1234")); + assert_eq!(masked.chars().filter(|c| *c == '\u{2022}').count(), 10); + assert_eq!(masked.chars().count(), 14); + } + + #[test] + fn mask_short_secret_fully() { + let masked = mask_secret("abc"); + assert_eq!(masked.chars().count(), 3); + assert!(masked.chars().all(|c| c == '\u{2022}')); + } + + #[test] + fn toml_roundtrip() { + let cfg = DtrackConfig { + url: "https://dt.example".into(), + api_key: "odt_abc1234".into(), + insecure: true, + }; + let raw = toml::to_string_pretty(&cfg).unwrap(); + let back = toml::from_str::(&raw).unwrap(); + assert_eq!(back.url, cfg.url); + assert_eq!(back.api_key, cfg.api_key); + assert!(back.insecure); + } +}