diff --git a/Cargo.lock b/Cargo.lock index 4d07a8bce5..fd40f824ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,7 +138,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -149,7 +149,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -596,7 +596,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-tungstenite 0.29.0", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -918,6 +918,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1090,7 +1099,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "tokio", - "tower", + "tower 0.5.3", ] [[package]] @@ -1232,7 +1241,7 @@ dependencies = [ "reqwest 0.13.4", "rmcp 1.8.0", "rustls", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "similar", @@ -1244,6 +1253,19 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "buzz-feature-flags" +version = "0.1.0" +dependencies = [ + "buzz-core", + "http", + "launchdarkly-server-sdk", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "buzz-media" version = "0.1.0" @@ -1383,7 +1405,7 @@ dependencies = [ "sqlx", "thiserror 2.0.18", "tokio", - "tower", + "tower 0.5.3", "tower-http", "tracing", "tracing-subscriber", @@ -1449,7 +1471,7 @@ dependencies = [ "tokio", "tokio-tungstenite 0.29.0", "tokio-util", - "tower", + "tower 0.5.3", "tower-http", "tracing", "tracing-opentelemetry", @@ -1808,6 +1830,12 @@ dependencies = [ "half", ] +[[package]] +name = "cidr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579504560394e388085d0c080ea587dfa5c15f7e251b4d5247d1e1a61d1d6928" + [[package]] name = "cipher" version = "0.4.4" @@ -1912,7 +1940,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2552,7 +2580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2585,6 +2613,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "deltae" version = "0.3.2" @@ -2748,7 +2807,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2969,7 +3028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3024,6 +3083,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "eventsource-client" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96df8cfa11d3c8e4e1a48b81c9c600ecbe8c11d1326f41e1524cc4d42f7bf6d9" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "http", + "launchdarkly-sdk-transport", + "log", + "pin-project", + "rand 0.10.1", + "tokio", +] + [[package]] name = "evmap" version = "11.0.0" @@ -3397,7 +3473,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows-link 0.1.3", + "windows-link 0.2.1", "windows-result 0.4.1", ] @@ -3562,7 +3638,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3595,6 +3671,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -3643,6 +3725,30 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1 0.10.6", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heapify" version = "0.2.0" @@ -3921,6 +4027,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-http-proxy" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-rustls" version = "0.27.9" @@ -4203,6 +4328,17 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -4441,7 +4577,7 @@ dependencies = [ "iroh-base", "iroh-dns", "iroh-metrics", - "lru", + "lru 0.18.0", "n0-error", "n0-future", "noq", @@ -4487,6 +4623,60 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jni" version = "0.21.1" @@ -4717,7 +4907,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-util", - "tower", + "tower 0.5.3", "tower-http", "tracing", ] @@ -4745,6 +4935,77 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" +[[package]] +name = "launchdarkly-sdk-transport" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "032d66802c461d7254ed1de887be6ca98ebbe126867d9a60c754baae81c4d10d" +dependencies = [ + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "log", + "no-proxy", + "tower 0.4.13", +] + +[[package]] +name = "launchdarkly-server-sdk" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22259877ec581fa56a9f0b982e583e16fa01b71ba6d3f786ef5ccfc6ca6f92ff" +dependencies = [ + "aws-lc-rs", + "bitflags 2.13.0", + "bytes", + "chrono", + "crossbeam-channel", + "data-encoding", + "eventsource-client", + "flate2", + "futures", + "http", + "launchdarkly-sdk-transport", + "launchdarkly-server-sdk-evaluation", + "log", + "lru 0.16.4", + "moka", + "parking_lot", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "uuid", +] + +[[package]] +name = "launchdarkly-server-sdk-evaluation" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76560a130b2a7e7fdb9c56043168844cde483e08e7f0e4a5e178e3888edc4336" +dependencies = [ + "base16ct 0.2.0", + "chrono", + "itertools", + "lazy_static", + "log", + "maplit", + "regex", + "semver", + "serde", + "serde_json", + "serde_with", + "sha1 0.10.6", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -4904,6 +5165,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + [[package]] name = "lru" version = "0.18.0" @@ -4960,6 +5227,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + [[package]] name = "matchers" version = "0.2.0" @@ -5277,7 +5550,7 @@ dependencies = [ "rpassword", "rustls", "same-file", - "schemars", + "schemars 1.2.1", "semver", "serde", "serde_json", @@ -5386,7 +5659,7 @@ dependencies = [ "prost-build 0.14.3", "protoc-bin-vendored", "rmcp 3.2.0", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "tokio", @@ -5590,7 +5863,7 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", - "indexmap", + "indexmap 2.14.0", "ipnet", "metrics", "metrics-util", @@ -5611,7 +5884,7 @@ dependencies = [ "crossbeam-epoch", "crossbeam-utils", "hashbrown 0.16.1", - "indexmap", + "indexmap 2.14.0", "metrics", "ordered-float 5.3.0", "quanta", @@ -6089,6 +6362,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no-proxy" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd3f71e44f009513a82ac2612c0c2da7e2924fad776931164317baa9695e150" +dependencies = [ + "cidr", +] + [[package]] name = "nom" version = "7.1.3" @@ -6241,7 +6523,7 @@ dependencies = [ "async-wsocket", "faster-hex", "futures", - "lru", + "lru 0.18.0", "negentropy", "nostr 0.45.1", "nostr-database", @@ -6269,7 +6551,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6986,7 +7268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset 0.5.7", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -6997,7 +7279,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset 0.5.7", "hashbrown 0.15.5", - "indexmap", + "indexmap 2.14.0", ] [[package]] @@ -7143,7 +7425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", - "indexmap", + "indexmap 2.14.0", "quick-xml 0.39.4", "serde", "time", @@ -7359,7 +7641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" dependencies = [ "futures", - "indexmap", + "indexmap 2.14.0", "nix 0.31.3", "tokio", "tracing", @@ -7890,7 +8172,7 @@ dependencies = [ "hashbrown 0.17.1", "itertools", "kasuari", - "lru", + "lru 0.18.0", "palette", "serde", "strum", @@ -8167,7 +8449,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -8214,7 +8496,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", - "tower", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -8282,7 +8564,7 @@ dependencies = [ "pin-project-lite", "process-wrap", "rmcp-macros 1.8.0", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "thiserror 2.0.18", @@ -8306,14 +8588,14 @@ dependencies = [ "http", "http-body", "http-body-util", - "indexmap", + "indexmap 2.14.0", "pastey", "pin-project-lite", "process-wrap", "rand 0.10.1", "reqwest 0.13.4", "rmcp-macros 3.2.0", - "schemars", + "schemars 1.2.1", "serde", "serde_json", "sse-stream", @@ -8465,7 +8747,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8524,7 +8806,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8634,6 +8916,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + [[package]] name = "schemars" version = "1.2.1" @@ -8807,7 +9101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8951,13 +9245,46 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "jiff", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -9365,7 +9692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9474,7 +9801,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "hashlink", - "indexmap", + "indexmap 2.14.0", "log", "memchr", "percent-encoding", @@ -9511,7 +9838,7 @@ dependencies = [ "futures-util", "hashbrown 0.16.1", "hashlink", - "indexmap", + "indexmap 2.14.0", "log", "memchr", "percent-encoding", @@ -10035,7 +10362,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10048,7 +10375,7 @@ dependencies = [ "parking_lot", "rustix", "signal-hook", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10446,7 +10773,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "indexmap", + "indexmap 2.14.0", "serde_core", "serde_spanned", "toml_datetime", @@ -10470,7 +10797,7 @@ version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap", + "indexmap 2.14.0", "toml_datetime", "toml_parser", "toml_writer", @@ -10516,7 +10843,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-stream", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -10544,6 +10871,17 @@ dependencies = [ "tonic", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -10552,7 +10890,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -10586,7 +10924,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -10790,7 +11128,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11353,7 +11691,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -12277,7 +12615,7 @@ checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "crc32fast", "flate2", - "indexmap", + "indexmap 2.14.0", "memchr", "typed-path", "zopfli", diff --git a/Cargo.toml b/Cargo.toml index fa1f9aa3e0..d27c63d234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/buzz-ws-client", "crates/buzz-admin", "crates/buzz-deletion", + "crates/buzz-feature-flags", "crates/buzz-workflow", "crates/buzz-media", "crates/buzz-cli", @@ -142,6 +143,7 @@ buzz-core = { path = "crates/buzz-core" } buzz-conformance = { path = "crates/buzz-conformance" } buzz-db = { path = "crates/buzz-db" } buzz-deletion = { path = "crates/buzz-deletion" } +buzz-feature-flags = { path = "crates/buzz-feature-flags" } buzz-auth = { path = "crates/buzz-auth" } buzz-pubsub = { path = "crates/buzz-pubsub" } buzz-search = { path = "crates/buzz-search" } diff --git a/Justfile b/Justfile index aff819573c..c53ebb70e7 100644 --- a/Justfile +++ b/Justfile @@ -414,6 +414,10 @@ test-unit: # because nothing in CI runs `cargo test --workspace` — workspace # membership alone buys clippy/check, not a single executed test. cargo nextest run -p buzz-backend-kubernetes + # Feature-flag crate coverage: run once with LaunchDarkly enabled. + # This includes all default tests plus the cfg(feature="launchdarkly") + # tests, avoiding duplicate default-only execution in the unit lane. + cargo nextest run -p buzz-feature-flags --features launchdarkly # buzz-agent: two infra-free concerns run together by executing the # whole crate (lib + integration tests), because nothing in CI runs # `cargo test --workspace`, so without this stanza neither the crate's diff --git a/crates/buzz-feature-flags/Cargo.toml b/crates/buzz-feature-flags/Cargo.toml new file mode 100644 index 0000000000..258b54cf6d --- /dev/null +++ b/crates/buzz-feature-flags/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "buzz-feature-flags" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +description = "Typed feature-flag contracts and evaluators for Buzz" + +[features] +launchdarkly = ["dep:launchdarkly-server-sdk"] + +[dependencies] +buzz-core = { workspace = true } +http = "1" +launchdarkly-server-sdk = { version = "=3.2.0", optional = true } +thiserror = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } +tempfile = "3" +tokio = { workspace = true } diff --git a/crates/buzz-feature-flags/README.md b/crates/buzz-feature-flags/README.md new file mode 100644 index 0000000000..6210701a6e --- /dev/null +++ b/crates/buzz-feature-flags/README.md @@ -0,0 +1,339 @@ +# buzz-feature-flags + +Provider-neutral, typed feature-flag contracts for Buzz server crates. + +This crate is infrastructure-only. It defines a small API for flag evaluation, +plus a static evaluator and an optional LaunchDarkly adapter behind a compile +feature. + +## Purpose + +`buzz-feature-flags` centralizes typed flag evaluation so server crates can use +flags without importing vendor SDK types. + +Provider-neutral types: + +- `BooleanFlag` +- `IntegerFlag` (`i64`, including negative values) +- `EvaluationContext` (required `CommunityId`, optional actor `PublicKey`) +- `FlagEvaluator` (typed `evaluate_bool` and `evaluate_int`) +- `StaticEvaluator` (always returns declared defaults) +- `EnvironmentEvaluator` (process-wide immutable environment snapshot) +- `EnvironmentDiagnostic` (sanitized malformed-value report) + +LaunchDarkly support is optional and compile-gated behind +`buzz-feature-flags/launchdarkly`. + +`EnvironmentEvaluator` is provider-neutral and always available (no Cargo +feature and no external dependency). + +## Environment Variable Naming + +`EnvironmentEvaluator` reads only the fixed namespace +`BUZZ_FEATURE_FLAG_`. + +For each flag key: + +- uppercase ASCII letters, +- replace each run of non-ASCII-alphanumeric characters with `_`, +- trim leading/trailing `_`, +- prepend `BUZZ_FEATURE_FLAG_`. + +Example: `relay.feature.query-v2` → `BUZZ_FEATURE_FLAG_RELAY_FEATURE_QUERY_V2`. + +Normalized names must be unique across declared flags. Collisions (for example +`a-b` and `a_b`) intentionally map to the same environment variable. Enforcing +uniqueness belongs in a future flag registry; this crate does not add a +registry yet. + +## Relay Artifact Contract (not yet wired into `buzz-relay`) + +The intended composition root is the outer relay artifact. It must select +exactly one provider at compile time: + +- `static-feature-flags` +- `environment-feature-flags` +- `launchdarkly-feature-flags` + +`launchdarkly-feature-flags` is the only relay feature that forwards to +`buzz-feature-flags/launchdarkly`. `static-feature-flags` and +`environment-feature-flags` keep the reusable crate vendor-neutral and still +allow adapter code to compile in tests. + +The relay artifact boundary, not this reusable crate, owns exact-one +enforcement. Consumer integration PRs must carry `exact-one compile_error guards` +verbatim/equivalently. That keeps `buzz-feature-flags` free to compile multiple +adapters for tests while ensuring a staging or production relay artifact cannot +choose a different evaluator at runtime. + +This repository currently adds the crate and adapter, but does **not** yet wire +relay `AppState` or handlers to consume it. `crates/buzz-relay/Cargo.toml` +still has no dependency on `buzz-feature-flags`, so current workspace/default +builds remain intentionally unchanged. The executable contract for the future +relay integration lives in +`tests/fixtures/relay-feature-selection`. + +## Composition Root Shape (artifact boundary) + +Construct once at process startup, then inject shared `Arc` +into server components. + +```rust +use std::sync::Arc; +use std::time::Duration; + +use buzz_feature_flags::{EnvironmentEvaluator, FlagEvaluator, StaticEvaluator}; +#[cfg(feature = "launchdarkly-feature-flags")] +use buzz_feature_flags::launchdarkly::{ + LaunchDarklyEvaluator, LaunchDarklyInitError, LaunchDarklyRuntimeConfig, + LaunchDarklyStartError, +}; + +#[cfg(not(any( + feature = "static-feature-flags", + feature = "environment-feature-flags", + feature = "launchdarkly-feature-flags", +)))] +compile_error!( + "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags" +); + +#[cfg(any( + all(feature = "static-feature-flags", feature = "environment-feature-flags"), + all(feature = "static-feature-flags", feature = "launchdarkly-feature-flags"), + all(feature = "environment-feature-flags", feature = "launchdarkly-feature-flags"), +))] +compile_error!( + "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags" +); + +#[cfg(any(feature = "static-feature-flags", feature = "environment-feature-flags"))] +#[derive(Default)] +struct FeatureFlagRuntimeConfig; + +#[cfg(feature = "launchdarkly-feature-flags")] +struct FeatureFlagRuntimeConfig { + sdk_key: String, + relay_proxy_endpoint: Option, +} + +struct RelayCompositionRoot { + feature_flags: Arc, + environment_diagnostics: Option>, + #[cfg(feature = "launchdarkly-feature-flags")] + launchdarkly_lifecycle: Option>, +} + +#[cfg(feature = "launchdarkly-feature-flags")] +enum RelayStartupError { + LaunchDarklyInit(LaunchDarklyInitError), + LaunchDarklyStart(LaunchDarklyStartError), +} + +#[cfg(feature = "static-feature-flags")] +fn build_feature_flag_evaluator(_config: FeatureFlagRuntimeConfig) -> RelayCompositionRoot { + RelayCompositionRoot { + feature_flags: Arc::new(StaticEvaluator), + environment_diagnostics: None, + } +} + +#[cfg(feature = "environment-feature-flags")] +fn build_feature_flag_evaluator(config: FeatureFlagRuntimeConfig) -> RelayCompositionRoot { + let _ = config; + let owner = Arc::new(EnvironmentEvaluator::from_process_environment()); + let feature_flags: Arc = owner.clone(); + RelayCompositionRoot { + feature_flags, + environment_diagnostics: Some(owner), + } +} + +#[cfg(feature = "launchdarkly-feature-flags")] +async fn build_feature_flag_evaluator( + config: FeatureFlagRuntimeConfig, +) -> Result { + let mut runtime_config = LaunchDarklyRuntimeConfig::new(config.sdk_key); + if let Some(relay_proxy_endpoint) = config.relay_proxy_endpoint { + runtime_config = runtime_config.with_relay_proxy_endpoint(relay_proxy_endpoint); + } + + let owner = Arc::new( + LaunchDarklyEvaluator::from_runtime_config(runtime_config) + .map_err(RelayStartupError::LaunchDarklyInit)?, + ); + owner + .start_with_default_executor_and_wait(Duration::from_secs(5)) + .await + .map_err(RelayStartupError::LaunchDarklyStart)?; + + let feature_flags: Arc = owner.clone(); + Ok(RelayCompositionRoot { + feature_flags, + environment_diagnostics: None, + launchdarkly_lifecycle: Some(owner), + }) +} +``` + +Each relay feature produces exactly one `build_feature_flag_evaluator` path, and +runtime config carries only the selected provider's settings. There is no +runtime provider enum, no provider-name environment variable, and no fallback +selector. + +Startup is fail-closed: if the selected provider fails initialization (including +`start_with_default_executor_and_wait` for LaunchDarkly), relay startup must +return an error and stop rather than falling back to another evaluator. + +The extra concrete `Arc` is an owner/lifecycle handle to that same evaluator, +not a layered provider. Consumers receive only the cloned `Arc` view. Retain the environment owner to drain diagnostics after +evaluations, and retain the LaunchDarkly owner so shutdown can call `close()`. + +`EnvironmentEvaluator` snapshots environment values once at construction, +retains only `BUZZ_FEATURE_FLAG_` entries, ignores `EvaluationContext`, and +never rereads process environment during evaluation. + +For deterministic tests or embedding, `EnvironmentEvaluator::from_pairs(...)` +constructs the same immutable snapshot from explicit key/value pairs. + +Environment entries are untyped at construction, so malformed present values +are detected when a typed flag is first evaluated. `take_diagnostics()` drains +each sanitized diagnostic at most once per flag key and expected type. The +composition root should retain the concrete environment evaluator and emit each +drained diagnostic as a warning through the application's logging stack. +Diagnostics contain no raw value or environment-variable name; the value +snapshot itself remains immutable. + +## `buzz-db` Boundary (approved correction) + +`buzz-db` may evaluate flags internally when choosing between equivalent query +implementations. It should depend on `buzz-feature-flags` without enabling the +LaunchDarkly feature, and it should never construct or import provider types. + +Concise example (query path selection only): + +```rust +use buzz_core::CommunityId; +use std::sync::Arc; + +use buzz_feature_flags::{EvaluationContext, FlagEvaluator, IntegerFlag}; + +pub struct Db { + feature_flags: Arc, +} + +impl Db { + pub async fn list_events( + &self, + community: CommunityId, + ) -> anyhow::Result> { + let context = EvaluationContext::for_community(community); + + let query_version = self.feature_flags.evaluate_int( + IntegerFlag::new("db.events.query-version", 1), + &context, + ); + + if query_version >= 2 { + self.list_events_v2_sql(community).await + } else { + self.list_events_v1_sql(community).await + } + } +} +``` + +SQL details are intentionally omitted in this example; the key point is that +public `Db` methods accept only domain inputs, while feature-flag evaluator +wiring remains internal to `Db` construction. + +`EvaluationContext::for_actor` is appropriate only when the existing DB +operation already receives an authoritative authenticated actor for domain +behavior. Feature targeting must not add actor/pubkey parameters to otherwise +actor-free DB APIs. + +For LaunchDarkly, the adapter context always includes kind `community` plus +optional global kind `pubkey`. Percentage rollouts must explicitly choose a +kind present in evaluation: rollouts must set `contextKind` to a kind present +in evaluation (`community` or `pubkey`) when they are intended to bucket Buzz +traffic. LaunchDarkly rollouts where configuration omits `contextKind` defaults +to `user`; Buzz does not synthesize a `user` alias, so those rollouts bucket as +zero and pick the first positive-weight variation. + +## Guardrails + +- Evaluation context inputs are authoritative server-resolved values + (`community`, optional `actor`), not client-supplied targeting attributes. +- Flags may choose between **equivalent** implementations only. +- Flags must not weaken authorization, tenant isolation, ordering guarantees, + transaction/consistency behavior, or schema invariants. +- Declared defaults choose the established safe path. +- Integer values must be validated at the consumer boundary before affecting + behavior. + +## Fallback, Startup, and Lifecycle + +- `StaticEvaluator` always returns each flag's declared default. +- `EnvironmentEvaluator` returns declared defaults when values are missing, + empty, invalid, non-Unicode, or the normalized flag key is unusable. Present + malformed values also produce a sanitized diagnostic; missing values do not. +- LaunchDarkly adapter returns declared defaults when a flag is missing, wrong + type, or evaluation fails. +- LaunchDarkly numbers are SDK `f64` values. Integer evaluation accepts only + mathematically integral values that convert to `i64` exactly from that `f64` + representation; otherwise it returns each flag's declared default. +- Startup chooses one evaluator at compile time (`StaticEvaluator`, + `EnvironmentEvaluator`, or LaunchDarkly); provider precedence/stacking is out + of scope. +- If LaunchDarkly is used, call evaluator `close()` during process shutdown. + `close()` blocks the calling thread while it flushes analytics, so async + shutdown code should offload it to a blocking shutdown path. +- Safety invariants must never rely on remote-flag availability. + +## Build and Test Expectations + +Build modes: + +```bash +# Provider-neutral (default): no LaunchDarkly dependency activated +cargo build -p buzz-feature-flags + +# Provider-neutral environment evaluator is included in the default build +cargo test -p buzz-feature-flags --test environment_evaluator + +# LaunchDarkly adapter enabled +cargo build -p buzz-feature-flags --features launchdarkly +``` + +Dependency-graph expectation checks: + +```bash +# Default graph should exclude launchdarkly-server-sdk +cargo tree -p buzz-feature-flags + +# Feature graph should include launchdarkly-server-sdk +cargo tree -p buzz-feature-flags --features launchdarkly +``` + +Testing expectations: + +- Run provider-neutral tests and LaunchDarkly-feature tests for this crate. +- Run the relay-artifact compile contract in + `tests/relay_feature_selection_contract.rs`, including zero-feature and + multiple-feature rejection. +- Keep `tests/fixtures/relay-feature-selection/Cargo.lock` checked in for the + nested `--locked` compile contract. Regenerate it from repo root with: + +```bash +cargo generate-lockfile --manifest-path crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml +``` + +- When `buzz-db` adopts flag-gated query selection, add parity tests proving + old/new query paths return equivalent rows, ordering, and transactional + behavior for the same inputs. + +## References + +- [ARCHITECTURE.md](../../ARCHITECTURE.md) +- [docs/multi-tenant-relay.md](../../docs/multi-tenant-relay.md) diff --git a/crates/buzz-feature-flags/src/environment.rs b/crates/buzz-feature-flags/src/environment.rs new file mode 100644 index 0000000000..ef00e9c487 --- /dev/null +++ b/crates/buzz-feature-flags/src/environment.rs @@ -0,0 +1,321 @@ +//! Process-wide environment-backed feature-flag evaluator. +//! +//! Environment variables use the fixed `BUZZ_FEATURE_FLAG_` namespace and a +//! normalized form of each flag key. +//! +//! Normalization algorithm: +//! - Uppercase ASCII letters. +//! - Replace each run of non-ASCII-alphanumeric characters with `_`. +//! - Trim leading and trailing `_`. +//! +//! If normalization yields an empty key segment, evaluation falls back to the +//! flag-declared default. +//! +//! Different flag keys can normalize to the same environment variable name +//! (for example `a-b` and `a_b`), so callers must keep normalized names unique. + +use std::{ + collections::{BTreeSet, HashMap}, + ffi::OsString, + fmt::{Debug, Formatter}, + sync::{Arc, Mutex}, +}; + +use crate::{BooleanFlag, EvaluationContext, FlagEvaluator, IntegerFlag}; + +const ENV_NAMESPACE: &str = "BUZZ_FEATURE_FLAG_"; + +#[derive(Clone)] +enum SnapshotValue { + Unicode(String), + NonUnicode, +} + +/// Sanitized diagnostic produced for a malformed configured environment value. +/// +/// Diagnostics identify only the provider-neutral flag key and expected type. +/// They never contain raw values or environment-variable names. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum EnvironmentDiagnostic { + /// A present value could not be evaluated as a boolean. + InvalidBooleanValue { + /// Stable provider-neutral flag key. + flag_key: &'static str, + }, + /// A present value could not be evaluated as a signed integer. + InvalidIntegerValue { + /// Stable provider-neutral flag key. + flag_key: &'static str, + }, +} + +#[derive(Default)] +struct DiagnosticState { + seen: BTreeSet, + pending: BTreeSet, +} + +/// Process-wide feature-flag evaluator backed by an immutable environment +/// snapshot. +/// +/// This evaluator captures key/value pairs once at construction and never +/// rereads or mutates process environment state afterward. Evaluation ignores +/// [`EvaluationContext`] because all values are process-wide. +#[derive(Clone)] +pub struct EnvironmentEvaluator { + snapshot: HashMap, + diagnostics: Arc>, +} + +impl Default for EnvironmentEvaluator { + fn default() -> Self { + Self::from_process_environment() + } +} + +impl EnvironmentEvaluator { + /// Capture a one-time snapshot of the current process environment. + /// + /// Only namespaced entries are retained. Entries with non-Unicode names are + /// ignored; non-Unicode values are retained only as invalid-value markers. + pub fn from_process_environment() -> Self { + Self::from_pairs(std::env::vars_os()) + } + + /// Build an evaluator from a deterministic key/value snapshot. + /// + /// Only entries whose names start with `BUZZ_FEATURE_FLAG_` are retained. + /// Entries with non-Unicode names are ignored; non-Unicode values are + /// retained only as invalid-value markers. + pub fn from_pairs(pairs: I) -> Self + where + I: IntoIterator, + K: Into, + V: Into, + { + let mut snapshot = HashMap::new(); + for (name, value) in pairs { + let name = name.into(); + let Some(name) = name.to_str() else { + continue; + }; + if !name.starts_with(ENV_NAMESPACE) { + continue; + } + + let value = match value.into().into_string() { + Ok(value) => SnapshotValue::Unicode(value), + Err(_) => SnapshotValue::NonUnicode, + }; + snapshot.insert(name.to_owned(), value); + } + + Self { + snapshot, + diagnostics: Arc::new(Mutex::new(DiagnosticState::default())), + } + } + + /// Drain sanitized diagnostics that have not previously been reported. + /// + /// Each malformed value is returned at most once per flag key and expected + /// type, including across cloned evaluators. Missing values do not produce + /// diagnostics. + pub fn take_diagnostics(&self) -> Vec { + let mut state = match self.diagnostics.lock() { + Ok(state) => state, + Err(poisoned) => poisoned.into_inner(), + }; + std::mem::take(&mut state.pending).into_iter().collect() + } + + fn lookup(&self, flag_key: &str) -> Option<&SnapshotValue> { + let env_name = normalize_flag_env_name(flag_key)?; + self.snapshot.get(&env_name) + } + + fn record_diagnostic(&self, diagnostic: EnvironmentDiagnostic) { + let mut state = match self.diagnostics.lock() { + Ok(state) => state, + Err(poisoned) => poisoned.into_inner(), + }; + if state.seen.insert(diagnostic) { + state.pending.insert(diagnostic); + } + } +} + +impl Debug for EnvironmentEvaluator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EnvironmentEvaluator") + .field("namespace", &ENV_NAMESPACE) + .field("entry_count", &self.snapshot.len()) + .field("entries", &"[REDACTED]") + .finish() + } +} + +impl FlagEvaluator for EnvironmentEvaluator { + fn evaluate_bool(&self, flag: BooleanFlag, _context: &EvaluationContext) -> bool { + let Some(value) = self.lookup(flag.key()) else { + return flag.default(); + }; + if let SnapshotValue::Unicode(value) = value { + if let Some(value) = parse_bool(value) { + return value; + } + } + + self.record_diagnostic(EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: flag.key(), + }); + flag.default() + } + + fn evaluate_int(&self, flag: IntegerFlag, _context: &EvaluationContext) -> i64 { + let Some(value) = self.lookup(flag.key()) else { + return flag.default(); + }; + if let SnapshotValue::Unicode(value) = value { + if let Some(value) = parse_i64(value) { + return value; + } + } + + self.record_diagnostic(EnvironmentDiagnostic::InvalidIntegerValue { + flag_key: flag.key(), + }); + flag.default() + } +} + +fn parse_bool(raw: &str) -> Option { + let trimmed = raw.trim(); + if trimmed.eq_ignore_ascii_case("true") { + Some(true) + } else if trimmed.eq_ignore_ascii_case("false") { + Some(false) + } else { + None + } +} + +fn parse_i64(raw: &str) -> Option { + raw.trim().parse::().ok() +} + +fn normalize_flag_env_name(flag_key: &str) -> Option { + let mut normalized = String::with_capacity(flag_key.len()); + let mut in_separator = false; + + for character in flag_key.chars() { + if character.is_ascii_alphanumeric() { + normalized.push(character.to_ascii_uppercase()); + in_separator = false; + } else if !in_separator { + normalized.push('_'); + in_separator = true; + } + } + + let trimmed = normalized.trim_matches('_'); + if trimmed.is_empty() { + return None; + } + + Some(format!("{ENV_NAMESPACE}{trimmed}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{BooleanFlag, EvaluationContext}; + use buzz_core::CommunityId; + + fn community(id: &str) -> CommunityId { + CommunityId::from_uuid(id.parse().expect("valid community UUID")) + } + + #[test] + fn normalization_uses_fixed_namespace_and_collapses_separators() { + assert_eq!( + normalize_flag_env_name("relay.feature.query-v2"), + Some("BUZZ_FEATURE_FLAG_RELAY_FEATURE_QUERY_V2".to_owned()) + ); + assert_eq!( + normalize_flag_env_name("___relay..foo---bar***baz??9___"), + Some("BUZZ_FEATURE_FLAG_RELAY_FOO_BAR_BAZ_9".to_owned()) + ); + } + + #[test] + fn normalization_rejects_empty_segment() { + assert_eq!(normalize_flag_env_name(""), None); + assert_eq!(normalize_flag_env_name("---***___"), None); + } + + #[test] + fn snapshot_retains_only_namespaced_entries() { + let evaluator = EnvironmentEvaluator::from_pairs([ + ("BUZZ_FEATURE_FLAG_RELAY_ENABLED", "true"), + ("DATABASE_URL", "sensitive"), + ]); + + assert_eq!(evaluator.snapshot.len(), 1); + assert!(evaluator + .snapshot + .contains_key("BUZZ_FEATURE_FLAG_RELAY_ENABLED")); + } + + #[cfg(unix)] + #[test] + fn non_unicode_names_are_ignored_and_values_become_invalid_markers() { + use std::os::unix::ffi::OsStringExt; + + let evaluator = EnvironmentEvaluator::from_pairs([ + ( + OsString::from("BUZZ_FEATURE_FLAG_RELAY_NON_UNICODE_NAME"), + OsString::from_vec(vec![0x80]), + ), + (OsString::from_vec(vec![0x81]), OsString::from("true")), + ]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!( + evaluator.evaluate_bool(BooleanFlag::new("relay.non-unicode-name", true), &context,) + ); + assert_eq!( + evaluator.take_diagnostics(), + vec![EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.non-unicode-name", + }] + ); + } + + #[test] + fn poisoned_diagnostic_mutex_recovers_without_panicking() { + let evaluator = EnvironmentEvaluator::from_pairs([( + "BUZZ_FEATURE_FLAG_RELAY_POISONED", + "not-a-boolean", + )]); + let diagnostics = Arc::clone(&evaluator.diagnostics); + let poison_result = std::thread::spawn(move || { + let _guard = diagnostics.lock().expect("diagnostic mutex lock"); + panic!("poison diagnostic mutex"); + }) + .join(); + assert!(poison_result.is_err()); + + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.poisoned", true), &context)); + assert_eq!( + evaluator.take_diagnostics(), + vec![EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.poisoned", + }] + ); + } +} diff --git a/crates/buzz-feature-flags/src/flags.rs b/crates/buzz-feature-flags/src/flags.rs new file mode 100644 index 0000000000..054e9a2abd --- /dev/null +++ b/crates/buzz-feature-flags/src/flags.rs @@ -0,0 +1,49 @@ +//! Typed feature-flag keys and declared defaults. + +/// Typed boolean feature definition. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BooleanFlag { + key: &'static str, + default: bool, +} + +impl BooleanFlag { + /// Construct a typed boolean flag from its stable key and declared default. + pub const fn new(key: &'static str, default: bool) -> Self { + Self { key, default } + } + + /// Stable key for this flag. + pub const fn key(self) -> &'static str { + self.key + } + + /// Declared default used when an evaluator is unavailable or cannot return a value. + pub const fn default(self) -> bool { + self.default + } +} + +/// Typed integer feature definition. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct IntegerFlag { + key: &'static str, + default: i64, +} + +impl IntegerFlag { + /// Construct a typed integer flag from its stable key and declared default. + pub const fn new(key: &'static str, default: i64) -> Self { + Self { key, default } + } + + /// Stable key for this flag. + pub const fn key(self) -> &'static str { + self.key + } + + /// Declared default used when an evaluator is unavailable or cannot return a value. + pub const fn default(self) -> i64 { + self.default + } +} diff --git a/crates/buzz-feature-flags/src/launchdarkly.rs b/crates/buzz-feature-flags/src/launchdarkly.rs new file mode 100644 index 0000000000..f5ce4a33c8 --- /dev/null +++ b/crates/buzz-feature-flags/src/launchdarkly.rs @@ -0,0 +1,953 @@ +use http::{uri::Authority, Uri}; +use launchdarkly_server_sdk::{ + BuildError, Client, ConfigBuildError, ConfigBuilder, Context, ContextBuilder, + MultiContextBuilder, +}; +use std::time::Duration; +use thiserror::Error; + +use crate::{BooleanFlag, EvaluationContext, FlagEvaluator, IntegerFlag}; + +const COMMUNITY_CONTEXT_KIND: &str = "community"; +const PUBKEY_CONTEXT_KIND: &str = "pubkey"; + +/// Runtime inputs required to initialize a LaunchDarkly-backed evaluator. +#[derive(Clone)] +pub struct LaunchDarklyRuntimeConfig { + /// SDK key used to authenticate with LaunchDarkly. + pub sdk_key: String, + /// Optional Relay Proxy endpoint used for all LaunchDarkly service traffic. + pub relay_proxy_endpoint: Option, +} + +impl std::fmt::Debug for LaunchDarklyRuntimeConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LaunchDarklyRuntimeConfig") + .field("sdk_key", &"[REDACTED]") + .field("relay_proxy_endpoint", &self.relay_proxy_endpoint) + .finish() + } +} + +impl LaunchDarklyRuntimeConfig { + /// Create a runtime config from explicit caller-provided values. + pub fn new(sdk_key: impl Into) -> Self { + Self { + sdk_key: sdk_key.into(), + relay_proxy_endpoint: None, + } + } + + /// Set the optional Relay Proxy endpoint. + pub fn with_relay_proxy_endpoint(mut self, endpoint: impl Into) -> Self { + self.relay_proxy_endpoint = Some(endpoint.into()); + self + } +} + +/// Errors building a LaunchDarkly evaluator from runtime inputs. +#[derive(Debug, Error)] +pub enum LaunchDarklyInitError { + /// Caller provided an empty SDK key. + #[error("launchdarkly sdk key must not be empty")] + EmptySdkKey, + /// Caller provided an empty relay-proxy endpoint. + #[error("launchdarkly relay proxy endpoint must not be empty")] + EmptyRelayProxyEndpoint, + /// Caller provided an invalid relay-proxy endpoint. + #[error("launchdarkly relay proxy endpoint must be an absolute http(s) URI with authority")] + InvalidRelayProxyEndpoint(InvalidRelayProxyEndpointReason), + /// LaunchDarkly SDK config build failed. + #[error("launchdarkly config build failed: {0}")] + ConfigBuild(#[from] ConfigBuildError), + /// LaunchDarkly SDK client build failed. + #[error("launchdarkly client build failed: {0}")] + ClientBuild(#[from] BuildError), +} + +/// Why a relay-proxy endpoint failed pre-validation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InvalidRelayProxyEndpointReason { + /// Endpoint could not be parsed as a URI. + MalformedUri, + /// Endpoint scheme is not `http` or `https`. + InvalidScheme, + /// Endpoint has no authority section (`host[:port]`). + MissingAuthority, +} + +/// Errors starting a LaunchDarkly evaluator and waiting for initialization. +#[derive(Debug, Error)] +pub enum LaunchDarklyStartError { + /// LaunchDarkly client did not initialize before timeout. + #[error("launchdarkly initialization timed out after {0:?}")] + InitializationTimeout(Duration), + /// LaunchDarkly client reported failed initialization. + #[error("launchdarkly initialization failed")] + InitializationFailed, +} + +/// Feature-flag evaluator backed by the LaunchDarkly Rust server SDK. +pub struct LaunchDarklyEvaluator { + client: Client, +} + +impl std::fmt::Debug for LaunchDarklyEvaluator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LaunchDarklyEvaluator").finish() + } +} + +impl LaunchDarklyEvaluator { + /// Wrap an already-constructed LaunchDarkly client. + pub(crate) fn new(client: Client) -> Self { + Self { client } + } + + /// Build a LaunchDarkly client from explicit runtime configuration. + pub fn from_runtime_config( + config: LaunchDarklyRuntimeConfig, + ) -> Result { + let sdk_key = config.sdk_key.trim(); + if sdk_key.is_empty() { + return Err(LaunchDarklyInitError::EmptySdkKey); + } + + let mut builder = ConfigBuilder::new(sdk_key); + if let Some(relay_proxy_endpoint) = config.relay_proxy_endpoint { + if relay_proxy_endpoint.trim().is_empty() { + return Err(LaunchDarklyInitError::EmptyRelayProxyEndpoint); + } + let relay_proxy_endpoint = validate_relay_proxy_endpoint(&relay_proxy_endpoint)?; + let mut service_endpoints = launchdarkly_server_sdk::ServiceEndpointsBuilder::new(); + service_endpoints.relay_proxy(&relay_proxy_endpoint); + builder = builder.service_endpoints(&service_endpoints); + } + + let config = builder.build()?; + let client = Client::build(config)?; + Ok(Self::new(client)) + } + + /// Start background tasks and wait for SDK initialization. + /// + /// This method must run inside a Tokio runtime. + pub async fn start_with_default_executor_and_wait( + &self, + timeout: Duration, + ) -> Result<(), LaunchDarklyStartError> { + self.client.start_with_default_executor(); + + match self.client.wait_for_initialization(timeout).await { + Some(true) => Ok(()), + Some(false) => Err(LaunchDarklyStartError::InitializationFailed), + None => Err(LaunchDarklyStartError::InitializationTimeout(timeout)), + } + } + + /// Gracefully stop background tasks and flush pending analytics events. + /// + /// This call blocks the current thread until pending analytics delivery + /// completes. Async callers should run it in a blocking shutdown path. + pub fn close(&self) { + self.client.close(); + } +} + +impl FlagEvaluator for LaunchDarklyEvaluator { + fn evaluate_bool(&self, flag: BooleanFlag, context: &EvaluationContext) -> bool { + let context = match launchdarkly_context(context) { + Ok(context) => context, + Err(_) => return flag.default(), + }; + self.client + .bool_variation(&context, flag.key(), flag.default()) + } + + fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64 { + let context = match launchdarkly_context(context) { + Ok(context) => context, + Err(_) => return flag.default(), + }; + + let detail = + self.client + .float_variation_detail(&context, flag.key(), flag.default() as f64); + + if detail.variation_index.is_none() { + return flag.default(); + } + + let Some(value) = detail.value else { + return flag.default(); + }; + + strict_f64_to_i64(value).unwrap_or_else(|| flag.default()) + } +} + +fn strict_f64_to_i64(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + + i64::try_from(value as i128).ok() +} + +fn validate_relay_proxy_endpoint(endpoint: &str) -> Result { + if endpoint.trim() != endpoint { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MalformedUri, + )); + } + + if endpoint.contains('#') { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MalformedUri, + )); + } + + let parsed = endpoint.parse::().map_err(|_| { + LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MalformedUri, + ) + })?; + + if !matches!(parsed.scheme_str(), Some("http" | "https")) { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::InvalidScheme, + )); + } + + if parsed.query().is_some() { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MalformedUri, + )); + } + + let Some(authority) = parsed.authority() else { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MissingAuthority, + )); + }; + + if authority.host().is_empty() { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MissingAuthority, + )); + } + + if authority_has_explicit_port(authority) && authority.port_u16().is_none() { + return Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::MalformedUri, + )); + } + + Ok(endpoint.to_owned()) +} + +fn authority_has_explicit_port(authority: &Authority) -> bool { + let authority = authority + .as_str() + .rsplit_once('@') + .map_or_else(|| authority.as_str(), |(_, host_and_port)| host_and_port); + + if authority.starts_with('[') { + return authority.contains("]:"); + } + + authority + .rsplit_once(':') + .map(|(_, port)| !port.is_empty()) + .unwrap_or(false) +} + +/// Build the LaunchDarkly context used for evaluation. +/// +/// The adapter always includes a `community` kind context and optionally adds a +/// global `pubkey` kind context when an actor is present. +fn launchdarkly_context(context: &EvaluationContext) -> Result { + let mut community = ContextBuilder::new(context.community().to_string()); + community.kind(COMMUNITY_CONTEXT_KIND); + let community = community.build()?; + + let Some(actor_pubkey) = context.actor_pubkey() else { + return Ok(community); + }; + + let mut actor = ContextBuilder::new(actor_pubkey.to_hex()); + actor.kind(PUBKEY_CONTEXT_KIND); + let actor = actor.build()?; + + MultiContextBuilder::of(vec![community, actor]).build() +} + +#[cfg(test)] +mod tests { + use super::*; + use buzz_core::{CommunityId, PublicKey}; + use launchdarkly_server_sdk::{ + Config, Flag, FlagBuilder, FlagValue, Kind, NullEventProcessorBuilder, TestData, + }; + + fn community(id: &str) -> CommunityId { + CommunityId::from_uuid(id.parse().expect("valid community UUID")) + } + + fn actor() -> PublicKey { + PublicKey::from_hex("5581946f95a03e6afb43027ec89b21507f040d35fd6f8594f168f4298f96f9cb") + .expect("valid pubkey") + } + + fn kind(name: &'static str) -> Kind { + Kind::try_from(name).expect("valid LaunchDarkly context kind") + } + + fn weighted_rollout_flag(key: &str, context_kind: Option<&str>) -> Flag { + let context_kind_json = context_kind.map_or_else(String::new, |context_kind| { + format!(",\"contextKind\":\"{context_kind}\"") + }); + + serde_json::from_str(&format!( + r#"{{ + "key": "{key}", + "version": 1, + "on": true, + "targets": [], + "rules": [], + "prerequisites": [], + "fallthrough": {{ + "rollout": {{ + "variations": [ + {{ "variation": 0, "weight": 1 }}, + {{ "variation": 1, "weight": 99999 }} + ] + {context_kind_json} + }} + }}, + "offVariation": 0, + "variations": [false, true], + "clientSideAvailability": {{ + "usingMobileKey": false, + "usingEnvironmentId": false + }}, + "salt": "saltyA" + }}"# + )) + .expect("valid preconfigured weighted rollout flag") + } + + fn test_config(test_data: &TestData) -> Config { + ConfigBuilder::new("sdk-key") + .data_source(test_data) + .event_processor(&NullEventProcessorBuilder::new()) + .build() + .expect("launchdarkly test config") + } + + fn default_test_config(test_data: &TestData) -> Config { + ConfigBuilder::new("sdk-key") + .data_source(test_data) + .build() + .expect("launchdarkly default test config") + } + + async fn started_evaluator(test_data: &TestData) -> LaunchDarklyEvaluator { + let config = test_config(test_data); + let client = Client::build(config).expect("launchdarkly test client"); + let evaluator = LaunchDarklyEvaluator::new(client); + evaluator + .start_with_default_executor_and_wait(Duration::from_secs(1)) + .await + .expect("launchdarkly initialized"); + evaluator + } + + #[test] + fn launchdarkly_test_config_uses_null_event_processor() { + let test_data = TestData::new(); + let config = test_config(&test_data); + let default_config = default_test_config(&test_data); + let endpoints = launchdarkly_server_sdk::ServiceEndpointsBuilder::new() + .build() + .expect("default service endpoints"); + let event_processor = config + .event_processor_builder() + .build(&endpoints, config.sdk_key(), None) + .expect("event processor build"); + let default_event_processor = default_config + .event_processor_builder() + .build(&endpoints, default_config.sdk_key(), None) + .expect("default event processor build"); + + event_processor.close(); + default_event_processor.close(); + assert!( + event_processor.flush_blocking(Duration::from_millis(10)), + "null event processor should remain a no-op after close" + ); + assert!( + !default_event_processor.flush_blocking(Duration::from_millis(10)), + "default event processor should fail flush_blocking after close" + ); + } + + #[test] + fn runtime_config_debug_redacts_sdk_key() { + let config = LaunchDarklyRuntimeConfig::new("super-secret-sdk-key") + .with_relay_proxy_endpoint("https://relay.internal"); + let debug = format!("{config:?}"); + + assert!(!debug.contains("super-secret-sdk-key")); + assert!(debug.contains("[REDACTED]")); + assert!(debug.contains("https://relay.internal")); + } + + #[test] + fn malformed_relay_proxy_endpoint_returns_typed_error_without_panic() { + let result = std::panic::catch_unwind(|| { + LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key") + .with_relay_proxy_endpoint("not a valid endpoint"), + ) + }); + + assert!( + result.is_ok(), + "invalid endpoint should return Err, not panic" + ); + assert!(matches!( + result.expect("catch_unwind result"), + Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint(_)) + )); + } + + #[test] + fn relay_proxy_endpoint_rejects_non_http_scheme_and_missing_authority() { + let invalid_scheme = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key") + .with_relay_proxy_endpoint("ftp://relay.internal:8030"), + ); + assert!(matches!( + invalid_scheme, + Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint( + InvalidRelayProxyEndpointReason::InvalidScheme + )) + )); + + for endpoint in [ + "https://", + "http:///missing-authority", + "https:///still-missing-authority", + "https://:8030/path-prefix", + ] { + let result = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key").with_relay_proxy_endpoint(endpoint), + ); + assert!(matches!( + result, + Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint(_)) + )); + } + } + + #[test] + fn relay_proxy_endpoint_rejects_query_fragment_and_invalid_ports() { + let invalid_cases = [ + "https://relay.internal/path?env=staging", + "https://relay.internal/path#fragment", + "https://relay.internal:notaport/path-prefix", + "https://relay.internal:99999/path-prefix", + ]; + + for endpoint in invalid_cases { + let error = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key").with_relay_proxy_endpoint(endpoint), + ) + .expect_err("endpoint should be rejected"); + + assert!(matches!( + error, + LaunchDarklyInitError::InvalidRelayProxyEndpoint(_) + )); + assert!(!format!("{error}").contains(endpoint)); + assert!(!format!("{error:?}").contains(endpoint)); + } + } + + #[test] + fn relay_proxy_endpoint_rejects_leading_or_trailing_whitespace() { + for endpoint in [ + " https://relay.internal:8030", + "https://relay.internal:8030 ", + "\thttps://relay.internal:8030", + "https://relay.internal:8030\n", + ] { + let result = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key").with_relay_proxy_endpoint(endpoint), + ); + assert!(matches!( + result, + Err(LaunchDarklyInitError::InvalidRelayProxyEndpoint(_)) + )); + } + } + + #[test] + fn relay_proxy_endpoint_accepts_absolute_http_and_https_uris() { + let valid_cases = [ + "http://relay.internal:8030", + "https://relay.internal", + "https://relay.internal:8443/path-prefix", + ]; + + for endpoint in valid_cases { + let result = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key").with_relay_proxy_endpoint(endpoint), + ); + + assert!( + result.is_ok(), + "expected valid endpoint {endpoint} to be accepted" + ); + } + } + + #[test] + fn relay_proxy_endpoint_error_does_not_leak_raw_endpoint_value() { + let endpoint = "https://bad endpoint with spaces"; + let error = LaunchDarklyEvaluator::from_runtime_config( + LaunchDarklyRuntimeConfig::new("sdk-key").with_relay_proxy_endpoint(endpoint), + ) + .expect_err("invalid endpoint should fail"); + + assert!(!format!("{error}").contains(endpoint)); + assert!(!format!("{error:?}").contains(endpoint)); + } + + #[tokio::test] + async fn community_only_context_targets_named_community_kind() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("community-only") + .fallthrough_variation(false) + .variation_for_key(kind("community"), community.to_string(), true), + ); + let evaluator = started_evaluator(&test_data).await; + + assert!(evaluator.evaluate_bool( + BooleanFlag::new("community-only", false), + &EvaluationContext::for_community(community), + )); + evaluator.close(); + } + + #[tokio::test] + async fn actor_context_targets_community_and_pubkey_kinds() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let actor = actor(); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("by-community") + .fallthrough_variation(false) + .variation_for_key(kind("community"), community.to_string(), true), + ); + test_data.update( + FlagBuilder::new("by-pubkey") + .fallthrough_variation(false) + .variation_for_key(kind("pubkey"), actor.to_hex(), true), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor(community, actor); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("by-community", false), &context)); + assert!(evaluator.evaluate_bool(BooleanFlag::new("by-pubkey", false), &context)); + evaluator.close(); + } + + #[tokio::test] + async fn same_actor_can_receive_different_variations_by_community() { + let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + let actor = actor(); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("community-rollout") + .fallthrough_variation(false) + .variation_for_key(kind("community"), community_a.to_string(), true), + ); + let evaluator = started_evaluator(&test_data).await; + let flag = BooleanFlag::new("community-rollout", false); + + assert!(evaluator.evaluate_bool(flag, &EvaluationContext::for_actor(community_a, actor),)); + assert!(!evaluator.evaluate_bool(flag, &EvaluationContext::for_actor(community_b, actor),)); + evaluator.close(); + } + + #[tokio::test] + async fn community_kind_weighted_rollout_uses_community_key_bucketing() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let test_data = TestData::new(); + test_data.use_preconfigured_flag(weighted_rollout_flag( + "community-weighted-rollout", + Some("community"), + )); + test_data + .use_preconfigured_flag(weighted_rollout_flag("missing-context-kind-rollout", None)); + + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_community(community); + + assert!(evaluator.evaluate_bool( + BooleanFlag::new("community-weighted-rollout", false), + &context, + )); + assert!(!evaluator.evaluate_bool( + BooleanFlag::new("missing-context-kind-rollout", true), + &context, + )); + + evaluator.close(); + } + + #[tokio::test] + async fn missing_flag_falls_back_to_declared_default() { + let test_data = TestData::new(); + let evaluator = started_evaluator(&test_data).await; + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.feature.missing", true), &context,)); + evaluator.close(); + } + + #[tokio::test] + async fn wrong_type_falls_back_to_declared_default() { + let test_data = TestData::new(); + test_data.update(FlagBuilder::new("relay.feature.bool").variation_for_all(true)); + test_data.update( + FlagBuilder::new("relay.feature.string") + .value_for_all(FlagValue::Str("red".to_owned())), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.feature.bool", false), &context,)); + assert!( + !evaluator.evaluate_bool(BooleanFlag::new("relay.feature.string", false), &context,) + ); + evaluator.close(); + } + + #[tokio::test] + async fn community_only_context_targets_integer_variation() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("community-int") + .variations([FlagValue::from(0_i64), FlagValue::from(13_i64)]) + .fallthrough_variation_index(0) + .variation_index_for_key(kind("community"), community.to_string(), 1), + ); + let evaluator = started_evaluator(&test_data).await; + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("community-int", -1), + &EvaluationContext::for_community(community), + ), + 13 + ); + evaluator.close(); + } + + #[tokio::test] + async fn actor_context_targets_integer_variation_by_community_and_pubkey() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let actor = actor(); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("by-community-int") + .variations([FlagValue::from(0_i64), FlagValue::from(8_i64)]) + .fallthrough_variation_index(0) + .variation_index_for_key(kind("community"), community.to_string(), 1), + ); + test_data.update( + FlagBuilder::new("by-pubkey-int") + .variations([FlagValue::from(0_i64), FlagValue::from(21_i64)]) + .fallthrough_variation_index(0) + .variation_index_for_key(kind("pubkey"), actor.to_hex(), 1), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor(community, actor); + + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("by-community-int", -1), &context), + 8 + ); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("by-pubkey-int", -1), &context), + 21 + ); + evaluator.close(); + } + + #[tokio::test] + async fn same_actor_can_receive_different_integer_variations_by_community() { + let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + let actor = actor(); + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("community-int-rollout") + .variations([FlagValue::from(-11_i64), FlagValue::from(99_i64)]) + .fallthrough_variation_index(0) + .variation_index_for_key(kind("community"), community_a.to_string(), 1), + ); + let evaluator = started_evaluator(&test_data).await; + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("community-int-rollout", -1), + &EvaluationContext::for_actor(community_a, actor), + ), + 99 + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("community-int-rollout", -1), + &EvaluationContext::for_actor(community_b, actor), + ), + -11 + ); + evaluator.close(); + } + + #[tokio::test] + async fn missing_integer_flag_falls_back_to_declared_default() { + let test_data = TestData::new(); + let evaluator = started_evaluator(&test_data).await; + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.feature.missing-int", -7), &context), + -7 + ); + evaluator.close(); + } + + #[tokio::test] + async fn missing_integer_flag_preserves_exact_declared_default_bytes() { + let test_data = TestData::new(); + let evaluator = started_evaluator(&test_data).await; + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + for default in [9_007_199_254_740_993_i64, i64::MAX, i64::MIN] { + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.missing-int-precise-default", default), + &context, + ), + default, + "missing-flag fallback must preserve exact declared default" + ); + } + + evaluator.close(); + } + + #[tokio::test] + async fn wrong_type_integer_falls_back_to_declared_default() { + let test_data = TestData::new(); + test_data + .update(FlagBuilder::new("relay.feature.int").value_for_all(FlagValue::from(17_i64))); + test_data.update( + FlagBuilder::new("relay.feature.int-bool").value_for_all(FlagValue::from(true)), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.feature.int", 0), &context), + 17 + ); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.feature.int-bool", -5), &context), + -5 + ); + evaluator.close(); + } + + #[tokio::test] + async fn wrong_type_integer_flag_preserves_exact_declared_default_bytes() { + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("relay.feature.int-wrong-type-for-precise-default") + .value_for_all(FlagValue::from(true)), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + for default in [9_007_199_254_740_993_i64, i64::MAX, i64::MIN] { + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-wrong-type-for-precise-default", default), + &context, + ), + default, + "wrong-type fallback must preserve exact declared default" + ); + } + + evaluator.close(); + } + + #[tokio::test] + async fn provider_not_ready_preserves_exact_declared_default_bytes() { + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("relay.feature.int-provider-not-ready") + .value_for_all(FlagValue::from(123_i64)), + ); + + let config = test_config(&test_data); + let client = Client::build(config).expect("launchdarkly test client"); + let evaluator = LaunchDarklyEvaluator::new(client); + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + for default in [9_007_199_254_740_993_i64, i64::MAX, i64::MIN] { + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-provider-not-ready", default), + &context, + ), + default, + "provider-not-ready fallback must preserve exact declared default" + ); + } + + evaluator.close(); + } + + #[tokio::test] + async fn fractional_integer_variation_falls_back_to_declared_default() { + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("relay.feature.int-fractional-positive") + .value_for_all(FlagValue::from(17.75_f64)), + ); + test_data.update( + FlagBuilder::new("relay.feature.int-fractional-negative") + .value_for_all(FlagValue::from(-17.75_f64)), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-fractional-positive", 123), + &context, + ), + 123 + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-fractional-negative", -456), + &context, + ), + -456 + ); + evaluator.close(); + } + + #[tokio::test] + async fn integer_variation_accepts_exact_large_f64_backed_i64_values() { + let test_data = TestData::new(); + test_data.update( + FlagBuilder::new("relay.feature.int-large-positive") + .value_for_all(FlagValue::from(1_152_921_504_606_846_976_i64)), + ); + test_data.update( + FlagBuilder::new("relay.feature.int-large-negative") + .value_for_all(FlagValue::from(-1_152_921_504_606_846_976_i64)), + ); + test_data.update( + FlagBuilder::new("relay.feature.int-min-boundary") + .value_for_all(FlagValue::from(i64::MIN)), + ); + test_data.update( + FlagBuilder::new("relay.feature.int-max-rounds-out-of-range") + .value_for_all(FlagValue::from(i64::MAX)), + ); + let evaluator = started_evaluator(&test_data).await; + let context = EvaluationContext::for_actor( + community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), + actor(), + ); + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-large-positive", 0), + &context, + ), + 1_152_921_504_606_846_976_i64 + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-large-negative", 0), + &context, + ), + -1_152_921_504_606_846_976_i64 + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-min-boundary", 11), + &context, + ), + i64::MIN + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("relay.feature.int-max-rounds-out-of-range", -12), + &context, + ), + -12 + ); + evaluator.close(); + } + + #[tokio::test] + async fn negative_integer_variation_is_supported() { + let community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let test_data = TestData::new(); + test_data.update(FlagBuilder::new("negative-int").value_for_all(FlagValue::from(-42_i64))); + let evaluator = started_evaluator(&test_data).await; + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("negative-int", 3), + &EvaluationContext::for_community(community), + ), + -42 + ); + evaluator.close(); + } +} diff --git a/crates/buzz-feature-flags/src/lib.rs b/crates/buzz-feature-flags/src/lib.rs new file mode 100644 index 0000000000..c9169b176b --- /dev/null +++ b/crates/buzz-feature-flags/src/lib.rs @@ -0,0 +1,217 @@ +#![deny(unsafe_code)] +#![warn(missing_docs)] +//! Typed boolean and integer feature flags for Buzz server components. + +use buzz_core::{CommunityId, PublicKey}; + +pub mod environment; +pub use environment::{EnvironmentDiagnostic, EnvironmentEvaluator}; + +pub mod flags; +pub use flags::{BooleanFlag, IntegerFlag}; + +#[cfg(feature = "launchdarkly")] +/// LaunchDarkly-backed evaluator adapter. +pub mod launchdarkly; + +/// Stable targeting context for one Buzz community and an optional actor. +/// +/// Community is always required because the same Nostr identity can participate +/// in multiple isolated Buzz communities. Community-only contexts support +/// background and system evaluations that have no actor. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EvaluationContext { + community: CommunityId, + actor_pubkey: Option, +} + +impl EvaluationContext { + /// Construct a community-only evaluation context. + pub const fn for_community(community: CommunityId) -> Self { + Self { + community, + actor_pubkey: None, + } + } + + /// Construct an evaluation context for an actor within one community. + pub const fn for_actor(community: CommunityId, actor_pubkey: PublicKey) -> Self { + Self { + community, + actor_pubkey: Some(actor_pubkey), + } + } + + /// Community that owns this evaluation. + pub const fn community(&self) -> CommunityId { + self.community + } + + /// Optional stable actor key for actor-targeted evaluation. + pub const fn actor_pubkey(&self) -> Option<&PublicKey> { + self.actor_pubkey.as_ref() + } +} + +/// Evaluates typed feature flags. +pub trait FlagEvaluator: Send + Sync { + /// Resolve a boolean flag for one Buzz evaluation context. + /// + /// Implementations must return [`BooleanFlag::default`] whenever no valid + /// boolean value is available, including provider absence or failure. + fn evaluate_bool(&self, flag: BooleanFlag, context: &EvaluationContext) -> bool; + + /// Resolve an integer flag for one Buzz evaluation context. + /// + /// Implementations must return [`IntegerFlag::default`] whenever no valid + /// integer value is available, including provider absence or failure. + fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64; +} + +/// Static evaluator that should return each flag's declared default. +#[derive(Debug, Default, Clone, Copy)] +pub struct StaticEvaluator; + +impl FlagEvaluator for StaticEvaluator { + fn evaluate_bool(&self, flag: BooleanFlag, _context: &EvaluationContext) -> bool { + flag.default() + } + + fn evaluate_int(&self, flag: IntegerFlag, _context: &EvaluationContext) -> i64 { + flag.default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use buzz_core::CommunityId; + + fn community(id: &str) -> CommunityId { + CommunityId::from_uuid(id.parse().expect("valid community UUID")) + } + + fn actor() -> PublicKey { + PublicKey::from_hex("c4f0623bdc8c4f7ecab9f7457f501f3e8f4efcf8f8f6ef6f4d76f42f5bb6f2cb") + .expect("valid pubkey") + } + + #[test] + fn static_evaluator_returns_declared_defaults_for_community() { + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(StaticEvaluator.evaluate_bool(BooleanFlag::new("enabled", true), &context)); + assert!(!StaticEvaluator.evaluate_bool(BooleanFlag::new("disabled", false), &context)); + } + + #[test] + fn static_evaluator_returns_declared_integer_defaults_for_community() { + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert_eq!( + StaticEvaluator.evaluate_int(IntegerFlag::new("positive", 7), &context), + 7 + ); + assert_eq!( + StaticEvaluator.evaluate_int(IntegerFlag::new("negative", -9), &context), + -9 + ); + } + + #[test] + fn non_launchdarkly_evaluator_can_target_community_through_public_trait() { + struct CommunityEvaluator { + enabled_community: CommunityId, + } + + impl FlagEvaluator for CommunityEvaluator { + fn evaluate_bool(&self, flag: BooleanFlag, context: &EvaluationContext) -> bool { + if context.community() == self.enabled_community { + true + } else { + flag.default() + } + } + + fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64 { + if context.community() == self.enabled_community { + 41 + } else { + flag.default() + } + } + } + + let enabled_community = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let other_community = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + let evaluator: &dyn FlagEvaluator = &CommunityEvaluator { enabled_community }; + + assert!(evaluator.evaluate_bool( + BooleanFlag::new("community-targeted-bool", false), + &EvaluationContext::for_community(enabled_community), + )); + assert!(!evaluator.evaluate_bool( + BooleanFlag::new("community-targeted-bool", false), + &EvaluationContext::for_community(other_community), + )); + + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("community-targeted-int", -1), + &EvaluationContext::for_community(enabled_community), + ), + 41 + ); + assert_eq!( + evaluator.evaluate_int( + IntegerFlag::new("community-targeted-int", -1), + &EvaluationContext::for_community(other_community), + ), + -1 + ); + } + + #[test] + fn same_actor_in_two_communities_is_distinguishable_for_integer_evaluation() { + struct CommunityRolloutEvaluator { + enabled_community: CommunityId, + expected_actor: PublicKey, + } + + impl FlagEvaluator for CommunityRolloutEvaluator { + fn evaluate_bool(&self, flag: BooleanFlag, _context: &EvaluationContext) -> bool { + flag.default() + } + + fn evaluate_int(&self, flag: IntegerFlag, context: &EvaluationContext) -> i64 { + if context.community() == self.enabled_community + && context.actor_pubkey() == Some(&self.expected_actor) + { + 99 + } else { + flag.default() + } + } + } + + let actor = actor(); + let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + let evaluator: &dyn FlagEvaluator = &CommunityRolloutEvaluator { + enabled_community: community_a, + expected_actor: actor, + }; + let flag = IntegerFlag::new("community-rollout", -11); + + assert_eq!( + evaluator.evaluate_int(flag, &EvaluationContext::for_actor(community_a, actor)), + 99 + ); + assert_eq!( + evaluator.evaluate_int(flag, &EvaluationContext::for_actor(community_b, actor)), + -11 + ); + } +} diff --git a/crates/buzz-feature-flags/tests/environment_evaluator.rs b/crates/buzz-feature-flags/tests/environment_evaluator.rs new file mode 100644 index 0000000000..501a8dd934 --- /dev/null +++ b/crates/buzz-feature-flags/tests/environment_evaluator.rs @@ -0,0 +1,336 @@ +use std::sync::Arc; + +use buzz_core::{CommunityId, PublicKey}; +use buzz_feature_flags::{ + BooleanFlag, EnvironmentDiagnostic, EnvironmentEvaluator, EvaluationContext, FlagEvaluator, + IntegerFlag, +}; + +const ENV_NAMESPACE: &str = "BUZZ_FEATURE_FLAG_"; + +fn community(id: &str) -> CommunityId { + CommunityId::from_uuid(id.parse().expect("valid community UUID")) +} + +fn actor(hex: &str) -> PublicKey { + PublicKey::from_hex(hex).expect("valid pubkey") +} + +#[test] +fn parses_booleans_case_insensitive_with_trim() { + let evaluator = EnvironmentEvaluator::from_pairs([ + ( + format!("{ENV_NAMESPACE}RELAY_BOOL_TRUE"), + " TrUe \t".to_owned(), + ), + ( + format!("{ENV_NAMESPACE}RELAY_BOOL_FALSE"), + "\n false ".to_owned(), + ), + ]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.true", false), &context)); + assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.bool.false", true), &context)); +} + +#[test] +fn parses_signed_negative_integers_with_trim() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}DB_EVENTS_QUERY_VERSION"), + " -17 ".to_owned(), + )]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("db.events.query-version", 1), &context), + -17 + ); +} + +#[test] +fn missing_flag_values_fall_back_to_declared_defaults() { + let evaluator = EnvironmentEvaluator::from_pairs([] as [(String, String); 0]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.missing", true), &context)); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.int.missing", -9), &context), + -9 + ); +} + +#[test] +fn invalid_and_empty_values_fall_back_to_declared_defaults() { + let evaluator = EnvironmentEvaluator::from_pairs([ + ( + format!("{ENV_NAMESPACE}RELAY_BOOL_INVALID"), + "maybe".to_owned(), + ), + (format!("{ENV_NAMESPACE}RELAY_BOOL_EMPTY"), " ".to_owned()), + ( + format!("{ENV_NAMESPACE}RELAY_INT_INVALID"), + "12.5".to_owned(), + ), + ]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.bool.invalid", false), &context)); + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.bool.empty", true), &context)); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.int.invalid", 88), &context), + 88 + ); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("!!!", -4), &context), + -4, + ); +} + +#[test] +fn evaluation_context_is_ignored_for_process_wide_values() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}RELAY_PROCESS_WIDE"), + "true".to_owned(), + )]); + let community_a = community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"); + let community_b = community("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"); + let actor_a = actor("c4f0623bdc8c4f7ecab9f7457f501f3e8f4efcf8f8f6ef6f4d76f42f5bb6f2cb"); + let actor_b = actor("f1e2d3c4b5a697887766554433221100ffeeddccbbaa99887766554433221100"); + + assert!(evaluator.evaluate_bool( + BooleanFlag::new("relay.process-wide", false), + &EvaluationContext::for_actor(community_a, actor_a), + )); + assert!(evaluator.evaluate_bool( + BooleanFlag::new("relay.process-wide", false), + &EvaluationContext::for_actor(community_b, actor_b), + )); + assert!(evaluator.evaluate_bool( + BooleanFlag::new("relay.process-wide", false), + &EvaluationContext::for_community(community_a), + )); +} + +#[test] +fn normalization_replaces_non_alnum_runs_and_trims_separators() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}RELAY_FOO_BAR_BAZ_9"), + "true".to_owned(), + )]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool( + BooleanFlag::new("___relay..foo---bar***baz??9___", false), + &context, + )); +} + +#[test] +fn colliding_flag_keys_share_normalized_environment_name() { + let evaluator = + EnvironmentEvaluator::from_pairs([(format!("{ENV_NAMESPACE}A_B"), "true".to_owned())]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("a-b", false), &context)); + assert!(evaluator.evaluate_bool(BooleanFlag::new("a_b", false), &context)); +} + +#[test] +fn snapshot_is_immutable_after_construction() { + let mut source = vec![(format!("{ENV_NAMESPACE}SNAPSHOT_BOOL"), "true".to_owned())]; + + let evaluator = EnvironmentEvaluator::from_pairs( + source + .iter() + .map(|(key, value)| (key.as_str(), value.as_str())), + ); + + source[0].1 = "false".to_owned(); + source.push((format!("{ENV_NAMESPACE}SNAPSHOT_INT"), "99".to_owned())); + + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + assert!(evaluator.evaluate_bool(BooleanFlag::new("snapshot.bool", false), &context)); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("snapshot.int", -1), &context), + -1 + ); +} + +#[test] +fn debug_does_not_expose_environment_contents() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}SECRET_FLAG"), + "super-secret-value".to_owned(), + )]); + + let debug = format!("{evaluator:?}"); + assert!(!debug.contains("super-secret-value")); + assert!(!debug.contains("SECRET_FLAG")); +} + +#[test] +fn malformed_present_values_default_and_diagnose_once_per_key_and_type() { + let evaluator = EnvironmentEvaluator::from_pairs([ + ( + format!("{ENV_NAMESPACE}RELAY_INVALID_BOOL"), + "raw-secret-bool".to_owned(), + ), + ( + format!("{ENV_NAMESPACE}RELAY_INVALID_INT"), + "raw-secret-int".to_owned(), + ), + ("DATABASE_URL".to_owned(), "unrelated-secret".to_owned()), + ]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + let bool_flag = BooleanFlag::new("relay.invalid-bool", true); + let int_flag = IntegerFlag::new("relay.invalid-int", -12); + + assert!(evaluator.evaluate_bool(bool_flag, &context)); + assert!(evaluator.evaluate_bool(bool_flag, &context)); + assert_eq!(evaluator.evaluate_int(int_flag, &context), -12); + assert_eq!(evaluator.evaluate_int(int_flag, &context), -12); + + let diagnostics = evaluator.take_diagnostics(); + assert_eq!( + diagnostics, + vec![ + EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.invalid-bool", + }, + EnvironmentDiagnostic::InvalidIntegerValue { + flag_key: "relay.invalid-int", + }, + ] + ); + + let rendered = format!("{diagnostics:?}"); + assert!(!rendered.contains("raw-secret-bool")); + assert!(!rendered.contains("raw-secret-int")); + assert!(!rendered.contains("unrelated-secret")); + assert!(!rendered.contains("DATABASE_URL")); + + assert!(evaluator.evaluate_bool(bool_flag, &context)); + assert_eq!(evaluator.evaluate_int(int_flag, &context), -12); + assert!(evaluator.take_diagnostics().is_empty()); +} + +#[test] +fn trait_object_evaluation_reports_through_retained_concrete_owner() { + let owner = Arc::new(EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}RELAY_INVALID"), + "not-a-boolean".to_owned(), + )])); + let evaluator: Arc = owner.clone(); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.invalid", true), &context)); + assert_eq!( + owner.take_diagnostics(), + vec![EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.invalid", + }] + ); +} + +#[test] +fn same_flag_key_reports_distinct_boolean_and_integer_diagnostics() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}RELAY_SHARED_KEY"), + "invalid-for-both-types".to_owned(), + )]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(!evaluator.evaluate_bool(BooleanFlag::new("relay.shared-key", false), &context)); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.shared-key", -4), &context), + -4 + ); + assert_eq!( + evaluator.take_diagnostics(), + vec![ + EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.shared-key", + }, + EnvironmentDiagnostic::InvalidIntegerValue { + flag_key: "relay.shared-key", + }, + ] + ); +} + +#[test] +fn cloned_evaluators_share_dedup_before_and_after_drain() { + let evaluator = EnvironmentEvaluator::from_pairs([( + format!("{ENV_NAMESPACE}RELAY_CLONED"), + "not-a-boolean".to_owned(), + )]); + let clone = evaluator.clone(); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + let flag = BooleanFlag::new("relay.cloned", false); + + assert!(!evaluator.evaluate_bool(flag, &context)); + assert!(!clone.evaluate_bool(flag, &context)); + assert_eq!( + clone.take_diagnostics(), + vec![EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.cloned", + }] + ); + assert!(evaluator.take_diagnostics().is_empty()); + + assert!(!evaluator.evaluate_bool(flag, &context)); + assert!(!clone.evaluate_bool(flag, &context)); + assert!(evaluator.take_diagnostics().is_empty()); + assert!(clone.take_diagnostics().is_empty()); +} + +#[test] +fn missing_values_default_without_diagnostics() { + let evaluator = EnvironmentEvaluator::from_pairs([] as [(String, String); 0]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + + assert!(evaluator.evaluate_bool(BooleanFlag::new("relay.missing-bool", true), &context)); + assert_eq!( + evaluator.evaluate_int(IntegerFlag::new("relay.missing-int", -4), &context), + -4 + ); + assert!(evaluator.take_diagnostics().is_empty()); +} + +#[cfg(unix)] +#[test] +fn namespaced_non_unicode_value_defaults_and_diagnoses_once() { + use std::{ffi::OsString, os::unix::ffi::OsStringExt}; + + let evaluator = EnvironmentEvaluator::from_pairs([( + OsString::from(format!("{ENV_NAMESPACE}RELAY_NON_UNICODE")), + OsString::from_vec(vec![0x80]), + )]); + let context = + EvaluationContext::for_community(community("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa")); + let flag = BooleanFlag::new("relay.non-unicode", false); + + assert!(!evaluator.evaluate_bool(flag, &context)); + assert!(!evaluator.evaluate_bool(flag, &context)); + assert_eq!( + evaluator.take_diagnostics(), + vec![EnvironmentDiagnostic::InvalidBooleanValue { + flag_key: "relay.non-unicode", + }] + ); + assert!(!evaluator.evaluate_bool(flag, &context)); + assert!(evaluator.take_diagnostics().is_empty()); +} diff --git a/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.lock b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.lock new file mode 100644 index 0000000000..6f0ec9a2bb --- /dev/null +++ b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.lock @@ -0,0 +1,2797 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6712f9c6fd6785b3b270884e57c441c403dc5d7e19ca45368c97c7a1de3000ec" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.3", + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "buzz-core" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "hmac 0.13.0", + "nostr", + "percent-encoding", + "rand 0.10.3", + "serde", + "serde_json", + "sha2 0.11.0", + "subtle", + "thiserror", + "url", + "uuid", + "zeroize", +] + +[[package]] +name = "buzz-feature-flags" +version = "0.1.0" +dependencies = [ + "buzz-core", + "http", + "launchdarkly-server-sdk", + "thiserror", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core 0.10.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cidr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579504560394e388085d0c080ea587dfa5c15f7e251b4d5247d1e1a61d1d6928" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.6", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "eventsource-client" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96df8cfa11d3c8e4e1a48b81c9c600ecbe8c11d1326f41e1524cc4d42f7bf6d9" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "http", + "launchdarkly-sdk-transport", + "log", + "pin-project", + "rand 0.10.3", + "tokio", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.2", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "headers" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" +dependencies = [ + "base64 0.22.1", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-http-proxy" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1d471ea2f65ba45eddb1d6ab7d58ac2671d2d4ac14da5c6516ae1d97e1327a" +dependencies = [ + "bytes", + "futures-util", + "headers", + "http", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "launchdarkly-sdk-transport" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "032d66802c461d7254ed1de887be6ca98ebbe126867d9a60c754baae81c4d10d" +dependencies = [ + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-http-proxy", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "log", + "no-proxy", + "tower", +] + +[[package]] +name = "launchdarkly-server-sdk" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22259877ec581fa56a9f0b982e583e16fa01b71ba6d3f786ef5ccfc6ca6f92ff" +dependencies = [ + "aws-lc-rs", + "bitflags 2.13.2", + "bytes", + "chrono", + "crossbeam-channel", + "data-encoding", + "eventsource-client", + "flate2", + "futures", + "http", + "launchdarkly-sdk-transport", + "launchdarkly-server-sdk-evaluation", + "log", + "lru", + "moka", + "parking_lot", + "rand 0.9.5", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "uuid", +] + +[[package]] +name = "launchdarkly-server-sdk-evaluation" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76560a130b2a7e7fdb9c56043168844cde483e08e7f0e4a5e178e3888edc4336" +dependencies = [ + "base16ct", + "chrono", + "itertools", + "lazy_static", + "log", + "maplit", + "regex", + "semver", + "serde", + "serde_json", + "serde_with", + "sha1", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "no-proxy" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd3f71e44f009513a82ac2612c0c2da7e2924fad776931164317baa9695e150" +dependencies = [ + "cidr", +] + +[[package]] +name = "nostr" +version = "0.44.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ff7b77ef428b40aa2834a6acbae38a0e104c98b306208ca4b87a420d579a4b" +dependencies = [ + "base64 0.22.1", + "bech32", + "bip39", + "bitcoin_hashes", + "cbc", + "chacha20 0.9.1", + "chacha20poly1305", + "getrandom 0.2.17", + "hex", + "instant", + "scrypt", + "secp256k1", + "serde", + "serde_json", + "unicode-normalization", + "url", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20 0.10.2", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "relay-feature-selection-fixture" +version = "0.0.0" +dependencies = [ + "buzz-feature-flags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "rand 0.8.8", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml new file mode 100644 index 0000000000..2f6cb93f20 --- /dev/null +++ b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "relay-feature-selection-fixture" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[features] +static-feature-flags = [] +environment-feature-flags = [] +launchdarkly-feature-flags = ["buzz-feature-flags/launchdarkly"] + +[dependencies] +buzz-feature-flags = { path = "../../.." } diff --git a/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/src/lib.rs b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/src/lib.rs new file mode 100644 index 0000000000..b979395fee --- /dev/null +++ b/crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/src/lib.rs @@ -0,0 +1,103 @@ +#![allow(dead_code)] + +use std::sync::Arc; + +use buzz_feature_flags::{EnvironmentEvaluator, FlagEvaluator}; +#[cfg(feature = "static-feature-flags")] +use buzz_feature_flags::StaticEvaluator; +#[cfg(feature = "launchdarkly-feature-flags")] +use buzz_feature_flags::launchdarkly::{ + LaunchDarklyEvaluator, LaunchDarklyInitError, LaunchDarklyRuntimeConfig, +}; + +#[cfg(not(any( + feature = "static-feature-flags", + feature = "environment-feature-flags", + feature = "launchdarkly-feature-flags", +)))] +compile_error!( + "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags" +); + +#[cfg(any( + all(feature = "static-feature-flags", feature = "environment-feature-flags"), + all(feature = "static-feature-flags", feature = "launchdarkly-feature-flags"), + all(feature = "environment-feature-flags", feature = "launchdarkly-feature-flags"), +))] +compile_error!( + "select exactly one relay feature flag provider feature: static-feature-flags, environment-feature-flags, or launchdarkly-feature-flags" +); + +#[cfg(any(feature = "static-feature-flags", feature = "environment-feature-flags"))] +#[derive(Clone, Debug, Default)] +pub struct FeatureFlagRuntimeConfig; + +#[cfg(feature = "launchdarkly-feature-flags")] +#[derive(Clone, Debug)] +pub struct FeatureFlagRuntimeConfig { + pub sdk_key: String, + pub relay_proxy_endpoint: Option, +} + +pub struct FeatureFlagCompositionRoot { + pub feature_flags: Arc, + pub environment_diagnostics: Option>, + #[cfg(feature = "launchdarkly-feature-flags")] + pub launchdarkly_lifecycle: Option>, +} + +#[cfg(feature = "static-feature-flags")] +pub fn build_feature_flag_evaluator(_config: FeatureFlagRuntimeConfig) -> FeatureFlagCompositionRoot { + FeatureFlagCompositionRoot { + feature_flags: Arc::new(StaticEvaluator), + environment_diagnostics: None, + } +} + +#[cfg(feature = "environment-feature-flags")] +pub fn build_feature_flag_evaluator( + _config: FeatureFlagRuntimeConfig, +) -> FeatureFlagCompositionRoot { + let owner = Arc::new(EnvironmentEvaluator::from_process_environment()); + let feature_flags: Arc = owner.clone(); + FeatureFlagCompositionRoot { + feature_flags, + environment_diagnostics: Some(owner), + } +} + +#[cfg(feature = "launchdarkly-feature-flags")] +pub fn build_feature_flag_evaluator( + config: FeatureFlagRuntimeConfig, +) -> Result { + let mut runtime_config = LaunchDarklyRuntimeConfig::new(config.sdk_key); + if let Some(relay_proxy_endpoint) = config.relay_proxy_endpoint { + runtime_config = runtime_config.with_relay_proxy_endpoint(relay_proxy_endpoint); + } + + let owner = Arc::new(LaunchDarklyEvaluator::from_runtime_config(runtime_config)?); + let feature_flags: Arc = owner.clone(); + Ok(FeatureFlagCompositionRoot { + feature_flags, + environment_diagnostics: None, + launchdarkly_lifecycle: Some(owner), + }) +} + +#[cfg(feature = "static-feature-flags")] +pub fn compile_smoke() { + let _ = build_feature_flag_evaluator(FeatureFlagRuntimeConfig); +} + +#[cfg(feature = "environment-feature-flags")] +pub fn compile_smoke() { + let _ = build_feature_flag_evaluator(FeatureFlagRuntimeConfig); +} + +#[cfg(feature = "launchdarkly-feature-flags")] +pub fn compile_smoke() { + let _ = build_feature_flag_evaluator(FeatureFlagRuntimeConfig { + sdk_key: "fixture-sdk-key".to_owned(), + relay_proxy_endpoint: None, + }); +} diff --git a/crates/buzz-feature-flags/tests/relay_feature_selection_contract.rs b/crates/buzz-feature-flags/tests/relay_feature_selection_contract.rs new file mode 100644 index 0000000000..120ad58787 --- /dev/null +++ b/crates/buzz-feature-flags/tests/relay_feature_selection_contract.rs @@ -0,0 +1,229 @@ +use std::{ + env, + ffi::OsString, + path::{Path, PathBuf}, + process::Command, +}; +use tempfile::TempDir; + +const FIXTURE_LOCK_REGEN_RECIPE: &str = + "cargo generate-lockfile --manifest-path crates/buzz-feature-flags/tests/fixtures/relay-feature-selection/Cargo.toml"; + +fn fixture_manifest() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/relay-feature-selection/Cargo.toml") +} + +fn fixture_lockfile() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/relay-feature-selection/Cargo.lock") +} + +fn crate_readme() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("README.md") +} + +fn cargo_bin() -> OsString { + env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo")) +} + +fn cargo_check(target_dir: &Path, case_name: &str, features: &[&str]) -> std::process::Output { + let mut command = Command::new(cargo_bin()); + command + .current_dir(env!("CARGO_MANIFEST_DIR")) + .arg("check") + .arg("--quiet") + .arg("--locked") + .arg("--manifest-path") + .arg(fixture_manifest()) + .arg("--no-default-features") + .env("CARGO_TARGET_DIR", target_dir); + + if !features.is_empty() { + command.arg("--features").arg(features.join(",")); + } + + command + .output() + .unwrap_or_else(|error| { + panic!( + "failed to run cargo check for {case_name}: {error}\nfixture lock regeneration recipe: `{FIXTURE_LOCK_REGEN_RECIPE}`" + ) + }) +} + +fn format_valid_case_failed_status(case_name: &str, stdout: &str, stderr: &str) -> String { + format!( + "expected {case_name} to compile successfully\nstdout:\n{stdout}\nstderr:\n{stderr}\nregenerate fixture lock with `{FIXTURE_LOCK_REGEN_RECIPE}`" + ) +} + +fn format_invalid_case_missing_exact_one(case_name: &str, stderr: &str) -> String { + format!( + "expected {case_name} failure to explain the exact-one contract\nstderr:\n{stderr}\nregenerate fixture lock with `{FIXTURE_LOCK_REGEN_RECIPE}`" + ) +} + +fn assert_valid_case_success(case_name: &str, output: &std::process::Output) { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "{}", + format_valid_case_failed_status(case_name, &stdout, &stderr) + ); +} + +fn assert_invalid_case_has_exact_one_diagnostic(case_name: &str, output: &std::process::Output) { + assert!( + !output.status.success(), + "expected {case_name} to fail compilation" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("select exactly one relay feature flag provider feature"), + "{}", + format_invalid_case_missing_exact_one(case_name, &stderr) + ); +} + +#[test] +fn relay_fixture_lock_regeneration_recipe_is_documented() { + let readme = std::fs::read_to_string(crate_readme()).expect("read crate README"); + assert!( + readme.contains(FIXTURE_LOCK_REGEN_RECIPE), + "README.md must document fixture lock regeneration with `{FIXTURE_LOCK_REGEN_RECIPE}`" + ); +} + +#[test] +fn relay_readme_documents_fail_closed_launchdarkly_startup_contract() { + let readme = std::fs::read_to_string(crate_readme()).expect("read crate README"); + + assert!( + readme.contains("start_with_default_executor_and_wait"), + "README.md must show LaunchDarkly startup via start_with_default_executor_and_wait" + ); + assert!( + readme.contains("async fn build_feature_flag_evaluator"), + "README.md must declare LaunchDarkly build_feature_flag_evaluator as async" + ); + assert!( + readme.contains( + "start_with_default_executor_and_wait(Duration::from_secs(5))\n .await" + ), + "README.md must await start_with_default_executor_and_wait" + ); + assert!( + readme.contains("fail-closed"), + "README.md must state fail-closed startup policy" + ); + assert!( + readme.contains("exact-one compile_error guards"), + "README.md must require exact-one compile_error guards in the consumer" + ); + assert!( + readme.contains("future flag registry"), + "README.md must defer environment-name uniqueness enforcement to a future registry" + ); + assert!( + readme.contains("kind `community` plus") + && readme.contains("optional global kind `pubkey`"), + "README.md must document LaunchDarkly adapter context kinds" + ); + assert!( + readme.contains("rollouts must set `contextKind` to a kind present") + && readme.contains("in evaluation"), + "README.md must document rollout contextKind requirements" + ); + assert!( + readme.contains("omits `contextKind` defaults") && readme.contains("to `user`;"), + "README.md must document LaunchDarkly default rollout context kind" + ); + assert!( + readme.contains("bucket as") + && readme.contains("zero and pick the first positive-weight variation"), + "README.md must document missing-kind rollout bucketing behavior" + ); + assert!( + readme.contains("close()` blocks the calling thread") + && readme.contains("while it flushes analytics"), + "README.md must document blocking LaunchDarkly close semantics" + ); + assert!( + readme.contains("offload it to a blocking shutdown path"), + "README.md must document async shutdown offloading guidance for close()" + ); +} + +#[test] +fn valid_case_failed_status_formatter_includes_fixture_lock_regeneration_recipe() { + let message = format_valid_case_failed_status( + "static", + "", + "error: lock file needs update but --locked was passed", + ); + assert!( + message.contains(FIXTURE_LOCK_REGEN_RECIPE), + "valid-case failure formatter must include fixture lock recipe\nmessage:\n{message}" + ); +} + +#[test] +fn invalid_case_missing_exact_one_formatter_includes_fixture_lock_regeneration_recipe() { + let message = + format_invalid_case_missing_exact_one("none", "error: unrelated nested cargo failure"); + assert!( + message.contains(FIXTURE_LOCK_REGEN_RECIPE), + "invalid-case failure formatter must include fixture lock recipe\nmessage:\n{message}" + ); +} + +#[test] +fn relay_artifact_provider_features_require_exactly_one_selection() { + assert!( + fixture_lockfile().is_file(), + "relay feature selection fixture must check in Cargo.lock for locked nested cargo runs\nregenerate it with `{FIXTURE_LOCK_REGEN_RECIPE}`" + ); + + let target_dir = TempDir::new().expect("temp target dir"); + + let valid_cases = [ + ("static", &["static-feature-flags"][..]), + ("environment", &["environment-feature-flags"][..]), + ("launchdarkly", &["launchdarkly-feature-flags"][..]), + ]; + + for (case_name, features) in valid_cases { + let output = cargo_check(target_dir.path(), case_name, features); + assert_valid_case_success(case_name, &output); + } + + let invalid_cases = [ + ("none", &[][..]), + ( + "static-and-environment", + &["static-feature-flags", "environment-feature-flags"][..], + ), + ( + "static-and-launchdarkly", + &["static-feature-flags", "launchdarkly-feature-flags"][..], + ), + ( + "environment-and-launchdarkly", + &["environment-feature-flags", "launchdarkly-feature-flags"][..], + ), + ( + "all-three", + &[ + "static-feature-flags", + "environment-feature-flags", + "launchdarkly-feature-flags", + ][..], + ), + ]; + + for (case_name, features) in invalid_cases { + let output = cargo_check(target_dir.path(), case_name, features); + assert_invalid_case_has_exact_one_diagnostic(case_name, &output); + } +} diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index a3d843b183..3732cdc476 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -137,6 +137,11 @@ run_unit_tests() { run_test_step "buzz-backend-kubernetes tests" \ cargo test -p buzz-backend-kubernetes -- --nocapture + # Keep fallback parity with `just test-unit`: one LaunchDarkly-feature run + # exercises both default and feature-gated buzz-feature-flags tests. + run_test_step "buzz-feature-flags tests (launchdarkly)" \ + cargo test -p buzz-feature-flags --features launchdarkly -- --nocapture + # buzz-agent model-capabilities corpus: the Rust half of the cross-language # drift guard. model_capabilities.rs embeds scripts/model-capabilities.json + # scripts/normative-corpus.json via include_str! and replays the full locked