diff --git a/.changeset/eso-manifests-parity.md b/.changeset/eso-manifests-parity.md deleted file mode 100644 index c1cdc45..0000000 --- a/.changeset/eso-manifests-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@smooai/config': minor ---- - -SMOODEV-1526: Port the ESO manifest generator (`buildClusterSecretStore` + `buildExternalSecret`) to the Go, Python, Rust, and C# SDKs for language parity with the TypeScript reference. Each emits the same ClusterSecretStore (webhook → real api.smoo.ai config-values endpoint) and per-workload ExternalSecret (secret-tier config keys → UPPER_SNAKE_CASE env vars, with overrides + duplicate guard), using each language's native snakecase util. Epic SMOODEV-1522. diff --git a/.changeset/eso-refresher-parity.md b/.changeset/eso-refresher-parity.md deleted file mode 100644 index 571c8b7..0000000 --- a/.changeset/eso-refresher-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@smooai/config': minor ---- - -SMOODEV-1526: Port the ESO bearer-token refresher core (the refresh algorithm + `SecretWriter` abstraction) to the Go, Python, Rust, and C# SDKs for parity with the TypeScript reference. Each mirrors the same behavior — invalidate-then-mint each cycle so the bootstrap Secret always holds a near-full-TTL token, fail-loud initial write, non-fatal loop-tick retries — driven by the language's own TokenProvider and unit-tested with a fake writer (no live cluster). The k8s-backed writer is intentionally an optional adapter so base SDK consumers don't pull a heavy k8s client; the TypeScript sidecar remains the canonical deployable. Epic SMOODEV-1522. diff --git a/CHANGELOG.md b/CHANGELOG.md index bb9812d..d237bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # @smooai/library-template +## 6.7.0 + +### Minor Changes + +- ad2c77f: SMOODEV-1526: Port the ESO manifest generator (`buildClusterSecretStore` + `buildExternalSecret`) to the Go, Python, Rust, and C# SDKs for language parity with the TypeScript reference. Each emits the same ClusterSecretStore (webhook → real api.smoo.ai config-values endpoint) and per-workload ExternalSecret (secret-tier config keys → UPPER_SNAKE_CASE env vars, with overrides + duplicate guard), using each language's native snakecase util. Epic SMOODEV-1522. +- ad2c77f: SMOODEV-1526: Port the ESO bearer-token refresher core (the refresh algorithm + `SecretWriter` abstraction) to the Go, Python, Rust, and C# SDKs for parity with the TypeScript reference. Each mirrors the same behavior — invalidate-then-mint each cycle so the bootstrap Secret always holds a near-full-TTL token, fail-loud initial write, non-fatal loop-tick retries — driven by the language's own TokenProvider and unit-tested with a fake writer (no live cluster). The k8s-backed writer is intentionally an optional adapter so base SDK consumers don't pull a heavy k8s client; the TypeScript sidecar remains the canonical deployable. Epic SMOODEV-1522. + ## 6.6.0 ### Minor Changes diff --git a/dotnet/src/SmooAI.Config/SmooAI.Config.csproj b/dotnet/src/SmooAI.Config/SmooAI.Config.csproj index d101285..0641bd4 100644 --- a/dotnet/src/SmooAI.Config/SmooAI.Config.csproj +++ b/dotnet/src/SmooAI.Config/SmooAI.Config.csproj @@ -12,7 +12,7 @@ SmooAI.Config - 6.6.0 + 6.7.0 SmooAI SmooAI SmooAI.Config diff --git a/go/config/version.go b/go/config/version.go index f7e9865..da98623 100644 --- a/go/config/version.go +++ b/go/config/version.go @@ -1,4 +1,4 @@ package config // Version is the current version of the smooai-config Go package. -const Version = "6.6.0" +const Version = "6.7.0" diff --git a/package.json b/package.json index 114351a..8755606 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@smooai/config", - "version": "6.6.0", + "version": "6.7.0", "description": "Type-safe multi-language configuration management with schema validation, three-tier config (public, secret, feature flags), and runtime client support for TypeScript, Python, Rust, and Go.", "homepage": "https://github.com/SmooAI/config#readme", "bugs": { diff --git a/python/pyproject.toml b/python/pyproject.toml index 2009370..db7bd31 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "smooai-config" -version = "6.6.0" +version = "6.7.0" description = "Smoo AI Configuration Management Library" requires-python = ">=3.13" dependencies = ["pydantic>=2.0.0", "httpx>=0.27.0", "cryptography>=42.0.0"] diff --git a/python/uv.lock b/python/uv.lock index 5774e41..29b366c 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -427,7 +427,7 @@ wheels = [ [[package]] name = "smooai-config" -version = "6.6.0" +version = "6.7.0" source = { editable = "." } dependencies = [ { name = "cryptography" }, diff --git a/rust/config/Cargo.lock b/rust/config/Cargo.lock index a59b6bc..0223b48 100644 --- a/rust/config/Cargo.lock +++ b/rust/config/Cargo.lock @@ -1307,7 +1307,7 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smooai-config" -version = "6.6.0" +version = "6.7.0" dependencies = [ "aes-gcm", "base64", diff --git a/rust/config/Cargo.toml b/rust/config/Cargo.toml index ad4dd1b..09eae9b 100644 --- a/rust/config/Cargo.toml +++ b/rust/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "smooai-config" -version = "6.6.0" +version = "6.7.0" edition = "2021" description = "Type-safe three-tier configuration management (public, secret, feature flags) with schema validation and a runtime client for the Smoo AI config platform." license = "MIT" diff --git a/rust/config/src/eso_manifests.rs b/rust/config/src/eso_manifests.rs index 3caa641..b1fee05 100644 --- a/rust/config/src/eso_manifests.rs +++ b/rust/config/src/eso_manifests.rs @@ -63,18 +63,14 @@ pub struct ClusterSecretStoreOptions { /// /// org + environment are baked into the URL because ESO's webhook only templates /// `{{ .remoteRef.key }}` per-secret — so a store is scoped to one (org, env). -pub fn build_cluster_secret_store( - opts: &ClusterSecretStoreOptions, -) -> Result { +pub fn build_cluster_secret_store(opts: &ClusterSecretStoreOptions) -> Result { if opts.api_url.is_empty() { return Err(SmooaiConfigError::new( "build_cluster_secret_store: api_url is required", )); } if opts.org_id.is_empty() { - return Err(SmooaiConfigError::new( - "build_cluster_secret_store: org_id is required", - )); + return Err(SmooaiConfigError::new("build_cluster_secret_store: org_id is required")); } if opts.environment.is_empty() { return Err(SmooaiConfigError::new( @@ -153,14 +149,9 @@ impl SecretMapping { /// Returns `(config_key, env_var)`. pub fn resolve_secret_mapping(m: &SecretMapping) -> Result<(String, String), SmooaiConfigError> { if m.config_key.is_empty() { - return Err(SmooaiConfigError::new( - "resolve_secret_mapping: config_key is required", - )); + return Err(SmooaiConfigError::new("resolve_secret_mapping: config_key is required")); } - let env_var = m - .env_var - .clone() - .unwrap_or_else(|| camel_to_upper_snake(&m.config_key)); + let env_var = m.env_var.clone().unwrap_or_else(|| camel_to_upper_snake(&m.config_key)); Ok((m.config_key.clone(), env_var)) } @@ -181,14 +172,10 @@ pub struct ExternalSecretOptions { /// @smooai/config key). pub fn build_external_secret(opts: &ExternalSecretOptions) -> Result { if opts.name.is_empty() { - return Err(SmooaiConfigError::new( - "build_external_secret: name is required", - )); + return Err(SmooaiConfigError::new("build_external_secret: name is required")); } if opts.namespace.is_empty() { - return Err(SmooaiConfigError::new( - "build_external_secret: namespace is required", - )); + return Err(SmooaiConfigError::new("build_external_secret: namespace is required")); } if opts.secrets.is_empty() { return Err(SmooaiConfigError::new( @@ -208,10 +195,7 @@ pub fn build_external_secret(opts: &ExternalSecretOptions) -> Result String { let mut out = String::with_capacity(s.len()); for b in s.bytes() { match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - out.push(b as char) - } + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char), b' ' => out.push_str("%20"), _ => { out.push('%'); @@ -311,8 +293,11 @@ mod tests { fn resolve_mapping_defaults_and_override() { let (_, env) = resolve_secret_mapping(&SecretMapping::new("mimoApiKey")).unwrap(); assert_eq!(env, "MIMO_API_KEY"); - let (_, env2) = - resolve_secret_mapping(&SecretMapping::with_env_var("alibabaModelStudioApiKey", "DASHSCOPE_API_KEY")).unwrap(); + let (_, env2) = resolve_secret_mapping(&SecretMapping::with_env_var( + "alibabaModelStudioApiKey", + "DASHSCOPE_API_KEY", + )) + .unwrap(); assert_eq!(env2, "DASHSCOPE_API_KEY"); } diff --git a/rust/config/src/eso_refresher.rs b/rust/config/src/eso_refresher.rs index 5040d71..6c88efb 100644 --- a/rust/config/src/eso_refresher.rs +++ b/rust/config/src/eso_refresher.rs @@ -23,10 +23,7 @@ pub const ESO_REFRESHER_DEFAULT_INTERVAL_SECONDS: u64 = 900; /// Writes the freshly-minted bearer token into the target Secret. Abstracted so /// the refresh loop is unit-testable without a live cluster. pub trait SecretWriter { - fn patch_bearer_token( - &self, - token: &str, - ) -> impl Future>; + fn patch_bearer_token(&self, token: &str) -> impl Future>; } /// The slice of `TokenProvider` the refresher needs. The real `TokenProvider` @@ -160,7 +157,11 @@ mod tests { #[tokio::test] async fn refresh_once_writes_fresh_token() { - let r = EsoRefresher::new(FakeTokenSource::new(&["tok-1"]), RecordingWriter::new(0), Duration::ZERO); + let r = EsoRefresher::new( + FakeTokenSource::new(&["tok-1"]), + RecordingWriter::new(0), + Duration::ZERO, + ); r.refresh_once().await.unwrap(); assert_eq!(*r.token_source.invalidations.borrow(), 1); assert_eq!(r.secret_writer.written.borrow().clone(), vec!["tok-1".to_string()]); @@ -168,23 +169,37 @@ mod tests { #[tokio::test] async fn forces_fresh_each_cycle() { - let r = EsoRefresher::new(FakeTokenSource::new(&["tok-1", "tok-2"]), RecordingWriter::new(0), Duration::ZERO); + let r = EsoRefresher::new( + FakeTokenSource::new(&["tok-1", "tok-2"]), + RecordingWriter::new(0), + Duration::ZERO, + ); r.refresh_once().await.unwrap(); r.refresh_once().await.unwrap(); assert_eq!(*r.token_source.calls.borrow(), 2); assert_eq!(*r.token_source.invalidations.borrow(), 2); - assert_eq!(r.secret_writer.written.borrow().clone(), vec!["tok-1".to_string(), "tok-2".to_string()]); + assert_eq!( + r.secret_writer.written.borrow().clone(), + vec!["tok-1".to_string(), "tok-2".to_string()] + ); } #[tokio::test] async fn refresh_once_propagates_write_failure() { - let r = EsoRefresher::new(FakeTokenSource::new(&["tok-1"]), RecordingWriter::new(1), Duration::ZERO); + let r = EsoRefresher::new( + FakeTokenSource::new(&["tok-1"]), + RecordingWriter::new(1), + Duration::ZERO, + ); assert!(r.refresh_once().await.is_err()); } #[tokio::test] async fn defaults_interval_when_zero() { let r = EsoRefresher::new(FakeTokenSource::new(&["t"]), RecordingWriter::new(0), Duration::ZERO); - assert_eq!(r.interval(), Duration::from_secs(ESO_REFRESHER_DEFAULT_INTERVAL_SECONDS)); + assert_eq!( + r.interval(), + Duration::from_secs(ESO_REFRESHER_DEFAULT_INTERVAL_SECONDS) + ); } }