diff --git a/Cargo.lock b/Cargo.lock index d2e0c684..9ff92bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,7 +23,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] @@ -39,6 +39,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", + "zeroize", +] + [[package]] name = "aes-gcm-siv" version = "0.11.1" @@ -192,12 +207,6 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" -[[package]] -name = "array-init" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" - [[package]] name = "arrayref" version = "0.3.9" @@ -321,9 +330,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -332,9 +341,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" dependencies = [ "cc", "cmake", @@ -544,6 +553,15 @@ 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-modes" version = "0.9.1" @@ -737,15 +755,15 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "inout", ] [[package]] name = "cipherstash-client" -version = "0.34.1-alpha.4" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3d67cc26d8422509d2c20644576124e7344a4bf14ded06c7affa8dc18aabca" +checksum = "af42b947b1abab17a1aa23d194a1b0154b5c338453e93ac70e2ab644407b907c" dependencies = [ "aes-gcm-siv", "anyhow", @@ -753,9 +771,9 @@ dependencies = [ "async-trait", "base16ct", "base64", + "base64ct", "base85", "blake3", - "cfg-if", "chrono", "cipherstash-config", "cipherstash-core", @@ -771,16 +789,12 @@ dependencies = [ "log", "miette", "opaque-debug", - "open 3.2.0", "orderable-bytes", "ore-rs", "percent-encoding", "rand 0.8.6", - "recipher 0.2.2", + "recipher 0.2.3", "reqwest", - "reqwest-middleware", - "reqwest-retry", - "reqwest-tracing", "rmp-serde", "rust-stemmers", "rust_decimal", @@ -800,7 +814,7 @@ dependencies = [ "url", "uuid", "vitaminc", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "winnow 0.6.26", "zeroize", "zerokms-protocol", @@ -808,9 +822,9 @@ dependencies = [ [[package]] name = "cipherstash-config" -version = "0.34.1-alpha.4" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283fa04db19f9bf2cb2f09e8c1505a15560310bc50fdc066734072c616aa8ca9" +checksum = "e255a8d6c38e4954146a02d07a109907cef39150073c3be4180a19b22ea6cd6c" dependencies = [ "bitflags", "serde", @@ -820,10 +834,11 @@ dependencies = [ [[package]] name = "cipherstash-core" -version = "0.34.1-alpha.4" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5bb7181053c3fc35569e0800fa7510c85ee2bffee21abbd2a7aeb498f5f0972" +checksum = "a0fb5c5b630e7fc2c716f25558ca8976b48d91b5ffb136c44d0d59e04f26c14d" dependencies = [ + "getrandom 0.2.15", "hmac", "lazy_static", "num-bigint", @@ -881,7 +896,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", - "vitaminc-protected", + "vitaminc-protected 0.1.0-pre4.2", "x509-parser", ] @@ -954,15 +969,14 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cllw-ore" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d007a5be83ae12adbd17543f9631d64090d761c029d2f8f7eb8f8ddb2a87caf" +checksum = "4f73a23cbc15404d9b314c03b16a888f798dbc681bceeb2e18674f602f9da02d" dependencies = [ "blake3", "chrono", "hex", "orderable-bytes", - "postgres-types", "rust_decimal", "subtle", "thiserror 1.0.69", @@ -977,7 +991,7 @@ checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" dependencies = [ "cipher", "dbl", - "digest", + "digest 0.10.7", ] [[package]] @@ -1157,6 +1171,15 @@ dependencies = [ "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 = "ctr" version = "0.9.2" @@ -1168,9 +1191,9 @@ dependencies = [ [[package]] name = "cts-common" -version = "0.34.1-alpha.4" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b26644e630f2e690194c6b61f5b613b768061750f2060cf4db73ddb8058d284" +checksum = "4fb189dfbe0822d67563bae6ede04a8d4c8dcd5de960c3fd41c6f7d2c3de2720" dependencies = [ "arrayvec", "axum", @@ -1182,6 +1205,7 @@ dependencies = [ "diesel", "either", "fake 3.1.0", + "getrandom 0.4.2", "http", "miette", "nom 8.0.0", @@ -1388,11 +1412,21 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "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", + "crypto-common 0.2.2", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1837,11 +1871,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -1976,7 +2022,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2025,6 +2071,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2565,7 +2620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -2847,16 +2902,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "open" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" -dependencies = [ - "pathdiff", - "windows-sys 0.42.0", -] - [[package]] name = "open" version = "5.3.3" @@ -3114,7 +3159,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" dependencies = [ - "array-init", "bytes", "chrono", "fallible-iterator", @@ -3388,9 +3432,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -3491,13 +3535,13 @@ dependencies = [ [[package]] name = "recipher" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b3561e1082283a4c064635b7886aa4d24db57a43ac31c55930c10797ab5cdeb" +checksum = "9398dce78ddfce08f93e9d9a3ac64d9b0a4fed478c0a82003c6e4c90dc245125" dependencies = [ "aes", - "async-trait", "cmac", + "getrandom 0.2.15", "hex", "hex-literal", "opaque-debug", @@ -3634,73 +3678,12 @@ dependencies = [ "web-sys", ] -[[package]] -name = "reqwest-middleware" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199dda04a536b532d0cc04d7979e39b1c763ea749bf91507017069c00b96056f" -dependencies = [ - "anyhow", - "async-trait", - "http", - "reqwest", - "serde", - "thiserror 2.0.18", - "tower-service", -] - -[[package]] -name = "reqwest-retry" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe2412db2af7d2268e7a5406be0431f37d9eb67ff390f35b395716f5f06c2eaa" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.15", - "http", - "hyper", - "reqwest", - "reqwest-middleware", - "retry-policies", - "thiserror 2.0.18", - "tokio", - "tracing", - "wasmtimer", -] - -[[package]] -name = "reqwest-tracing" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5c1a1510677d43dce9e9c0c07fc5db8772c0e5a43e4f9cef75a11affa05a578" -dependencies = [ - "anyhow", - "async-trait", - "getrandom 0.2.15", - "http", - "matchit", - "reqwest", - "reqwest-middleware", - "tracing", -] - [[package]] name = "resolv-conf" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" -[[package]] -name = "retry-policies" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a4bd6027df676bcb752d3724db0ea3c0c5fc1dd0376fec51ac7dcaf9cc69be" -dependencies = [ - "rand 0.9.2", -] - [[package]] name = "ring" version = "0.17.14" @@ -4192,7 +4175,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -4339,13 +4322,16 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stack-auth" -version = "0.34.1-alpha.4" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677a79b9f48a194a8fc24870d27af6b7c62280ac5fcf85967e12165f16690d9d" dependencies = [ "aquamarine", + "base64", "cts-common", "jsonwebtoken", "miette", - "open 5.3.3", + "open", "reqwest", "serde", "serde_json", @@ -4356,16 +4342,17 @@ dependencies = [ "url", "uuid", "vitaminc", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", + "web-time", "zeroize", "zerokms-protocol", ] [[package]] name = "stack-profile" -version = "0.34.1-alpha.4" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd61bc4129d2258ec1ba89d742558308560fc0f585f9c24d478685def8efd14" +checksum = "47f49a439a0bced8c2ff9b6e1e2cf448ec3e923fe9acdc6f40d80c796a208f21" dependencies = [ "dirs", "gethostname", @@ -4943,9 +4930,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.18.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unarray" @@ -5016,7 +5003,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] @@ -5095,9 +5082,11 @@ checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "atomic", "getrandom 0.3.2", + "js-sys", "md-5", "serde", "sha1_smol", + "wasm-bindgen", ] [[package]] @@ -5150,39 +5139,40 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8b739a2cb1e528e77a69267728532f52d2d5ce18ae2839e26c797859fe9015" +checksum = "d69481bc78bc3227d6c70d8aae6437c79badbf54fd9ec90c1b4ae2553068a989" dependencies = [ "vitaminc-aead", "vitaminc-encrypt", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "vitaminc-traits", ] [[package]] name = "vitaminc-aead" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c29cef4d4b0d018c4223d366017d2a9756012acf76e25011aaca877f3c74904" +checksum = "be80f3a3d83e69a786b97a831d660449a0437ccac3b3e369bf590afcb45569b0" dependencies = [ "bytes", "serde", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] [[package]] name = "vitaminc-encrypt" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e3869aaf60ebb95ccbdfcf003985132325b4d1ac6f5d945ad2fbb9149afd3a" +checksum = "7477ef8ac925a75aacf5dbddfd4b17fd32f35ee9fb4a7c45ac3db80fd9ad4006" dependencies = [ + "aes-gcm", "aws-lc-rs", "vitaminc-aead", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] @@ -5194,11 +5184,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af693c39d3cd1c818ef6267539433c6ceca87840b12d24124adbc9c8ecba1709" dependencies = [ "bitvec", - "digest", + "digest 0.10.7", + "serde", + "serde_bytes", + "subtle", + "vitaminc-protected-derive 0.1.0-pre4.2", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.2.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8472e2b76b5dedaf429708393964c3cc6f7ee40e6a43ed420288e3e4900c6af" +dependencies = [ + "bitvec", + "digest 0.11.3", "serde", "serde_bytes", "subtle", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.2.0-pre.1", "zeroize", ] @@ -5213,24 +5218,36 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "vitaminc-protected-derive" +version = "0.2.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b01e1715676d8bf606314c2a51df0793c01bd743bae4bc00643d68f766ee1e91" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "vitaminc-random" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9de431cb93359d293ec7e70d05d87117a57f34bfc5bc94f040b81d4dd1afd6" +checksum = "b0785c13f839240523ba8db6535384a5e8d4fe2b2f28bbddcfcb5fd6de825996" dependencies = [ - "rand 0.10.0", + "getrandom 0.4.2", + "rand 0.10.1", "thiserror 2.0.18", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random-derives", "zeroize", ] [[package]] name = "vitaminc-random-derives" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d33ac4682235551d25c874525c20e03d4c863b39f556391f52f7a2083bfbdf" +checksum = "01e750eefb1f49940f589b2d397e2323d5df4b62bfb33b4e40e1d20a35c3f167" dependencies = [ "proc-macro2", "quote", @@ -5239,16 +5256,16 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.1.0-pre4.2" +version = "0.2.0-pre.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25a9e51d24c3befddd71e907dd4ae9f21cfbaae065fb0ef5202e5d21cd198d0" +checksum = "3794e2c028cff00f40caea05ab6dce38181a94e13c0aaee640e7b867369780eb" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.18", - "vitaminc-protected", + "vitaminc-protected 0.2.0-pre.1", "vitaminc-random", "zeroize", ] @@ -5429,20 +5446,6 @@ dependencies = [ "semver", ] -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot", - "pin-utils", - "slab", - "wasm-bindgen", -] - [[package]] name = "web-sys" version = "0.3.77" @@ -5520,7 +5523,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -5666,21 +5669,6 @@ dependencies = [ "windows-link 0.1.1", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -6275,15 +6263,16 @@ dependencies = [ [[package]] name = "zerokms-protocol" -version = "0.12.9" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2f045e2ee975a3d448419245c4621ea8844d2a004c63a96277181dc7cf8483" +checksum = "3435005f9a76f20ba27158f49e8dde391d41b9557f12c6422a14e8e9ccda094d" dependencies = [ "base64", "cipherstash-config", "const-hex", "cts-common", "fake 2.10.0", + "getrandom 0.2.15", "opaque-debug", "rand 0.8.6", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5cbfa718..a6a7e142 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] resolver = "2" members = ["packages/*"] -# Vendored crate is consumed only via [patch.crates-io] below, not as a member. -exclude = ["vendor/stack-auth"] [workspace.package] version = "2.2.4" @@ -45,9 +43,9 @@ debug = true [workspace.dependencies] sqltk = { version = "0.10.0" } -cipherstash-client = { version = "=0.34.1-alpha.4" } -cipherstash-config = { version = "=0.34.1-alpha.4" } -cts-common = { version = "=0.34.1-alpha.4" } +cipherstash-client = { version = "0.38.0" } +cipherstash-config = { version = "0.38.0" } +cts-common = { version = "0.38.0" } thiserror = "2.0.9" tokio = { version = "1.44.2", features = ["full"] } @@ -58,13 +56,3 @@ tracing-subscriber = { version = "^0.3.20", features = [ "env-filter", "std", ] } - -# HOTFIX (CIP-3159): backport the stack-auth token-refresh CancelGuard fix onto -# the 0.34.1-alpha.4 source that cipherstash-client 0.34.1-alpha.4 pins. Without -# this, a cancelled get_token() future could strand `refresh_in_progress = true`, -# wedging all later refreshes and causing ZeroKMS "Request not authorized" exactly -# ~15 min (token TTL) after startup. The patch keeps version 0.34.1-alpha.4 so it -# satisfies cipherstash-client's exact pin while replacing the registry source. -# Remove once Proxy moves to a cipherstash-client built against stack-auth >= 0.36.0. -[patch.crates-io] -stack-auth = { path = "vendor/stack-auth" } diff --git a/mise.local.example.toml b/mise.local.example.toml index 6e72948a..39e82bb2 100644 --- a/mise.local.example.toml +++ b/mise.local.example.toml @@ -15,7 +15,7 @@ CS_CLIENT_KEY = "client-key" CS_CLIENT_ID = "client-id" # The release of EQL that the proxy tests will use and releases will be built with -CS_EQL_VERSION = "eql-2.3.0-pre.3" +CS_EQL_VERSION = "eql-2.3.1" # TLS variables are required for providing TLS to Proxy's clients. # CS_TLS__TYPE can be either "Path" or "Pem" (case-sensitive). diff --git a/mise.toml b/mise.toml index 75110dd1..1827980d 100644 --- a/mise.toml +++ b/mise.toml @@ -34,7 +34,7 @@ CS_PROXY__HOST = "host.docker.internal" # Misc DOCKER_CLI_HINTS = "false" # Please don't show us What's Next. -CS_EQL_VERSION = "eql-2.3.0-pre.3" +CS_EQL_VERSION = "eql-2.3.1" [tools] diff --git a/packages/cipherstash-proxy/src/error.rs b/packages/cipherstash-proxy/src/error.rs index aee61575..45c0a5e0 100644 --- a/packages/cipherstash-proxy/src/error.rs +++ b/packages/cipherstash-proxy/src/error.rs @@ -340,9 +340,9 @@ impl From for EncryptError { cipherstash_client::eql::EqlError::ColumnConfigurationMismatch { table, column } => { Self::ColumnConfigurationMismatch { table, column } } - cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset { keyset_id } => { - Self::CouldNotDecryptDataForKeyset { keyset_id } - } + cipherstash_client::eql::EqlError::CouldNotDecryptDataForKeyset { + keyset_id, .. + } => Self::CouldNotDecryptDataForKeyset { keyset_id }, cipherstash_client::eql::EqlError::InvalidIndexTerm => Self::InvalidIndexTerm, cipherstash_client::eql::EqlError::MissingCiphertext(identifier) => { Self::ColumnCouldNotBeDeserialised { diff --git a/packages/cipherstash-proxy/src/lib.rs b/packages/cipherstash-proxy/src/lib.rs index 2d4ac8fa..5d230a70 100644 --- a/packages/cipherstash-proxy/src/lib.rs +++ b/packages/cipherstash-proxy/src/lib.rs @@ -16,7 +16,7 @@ pub use crate::config::{DatabaseConfig, ServerConfig, TandemConfig, TlsConfig}; pub use crate::log::init; pub use crate::proxy::Proxy; pub use cipherstash_client::encryption::Plaintext; -pub use cipherstash_client::eql::{EqlCiphertext, Identifier}; +pub use cipherstash_client::eql::{EqlCiphertext, EqlOutput, Identifier}; use std::mem; diff --git a/packages/cipherstash-proxy/src/postgresql/backend.rs b/packages/cipherstash-proxy/src/postgresql/backend.rs index b1a5c4b1..00eecac6 100644 --- a/packages/cipherstash-proxy/src/postgresql/backend.rs +++ b/packages/cipherstash-proxy/src/postgresql/backend.rs @@ -538,7 +538,7 @@ where for (col, ct) in projection_columns.iter().zip(ciphertexts) { match (col, ct) { (Some(col), Some(ct)) => { - if col.identifier != ct.identifier { + if &col.identifier != ct.identifier() { return Err(EncryptError::ColumnConfigurationMismatch { table: col.identifier.table.to_owned(), column: col.identifier.column.to_owned(), @@ -553,8 +553,8 @@ where // ciphertext with no column configuration is bad (None, Some(ct)) => { return Err(EncryptError::ColumnConfigurationMismatch { - table: ct.identifier.table.to_owned(), - column: ct.identifier.column.to_owned(), + table: ct.identifier().table.to_owned(), + column: ct.identifier().column.to_owned(), } .into()); } @@ -749,7 +749,7 @@ mod tests { _keyset_id: Option, _plaintexts: Vec>, _columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { Ok(vec![]) } diff --git a/packages/cipherstash-proxy/src/postgresql/context/mod.rs b/packages/cipherstash-proxy/src/postgresql/context/mod.rs index ab3052b5..f9cd0ebe 100644 --- a/packages/cipherstash-proxy/src/postgresql/context/mod.rs +++ b/packages/cipherstash-proxy/src/postgresql/context/mod.rs @@ -752,7 +752,7 @@ where &self, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { let keyset_id = self.keyset_identifier(); self.encryption @@ -1077,7 +1077,7 @@ mod tests { _keyset_id: Option, _plaintexts: Vec>, _columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { Ok(vec![]) } diff --git a/packages/cipherstash-proxy/src/postgresql/frontend.rs b/packages/cipherstash-proxy/src/postgresql/frontend.rs index 87199677..3fb24af1 100644 --- a/packages/cipherstash-proxy/src/postgresql/frontend.rs +++ b/packages/cipherstash-proxy/src/postgresql/frontend.rs @@ -27,7 +27,7 @@ use crate::prometheus::{ STATEMENTS_PASSTHROUGH_TOTAL, STATEMENTS_UNMAPPABLE_TOTAL, }; use crate::proxy::EncryptionService; -use crate::EqlCiphertext; +use crate::EqlOutput; use bytes::BytesMut; use cipherstash_client::encryption::Plaintext; use eql_mapper::{self, EqlMapperError, EqlTerm, TypeCheckedStatement}; @@ -582,13 +582,13 @@ where /// # Returns /// /// Vector of encrypted values corresponding to each literal, with `None` for - /// literals that don't require encryption and `Some(EqlCiphertext)` for encrypted values. + /// literals that don't require encryption and `Some(EqlOutput)` for encrypted values. async fn encrypt_literals( &mut self, session_id: SessionId, typed_statement: &TypeCheckedStatement<'_>, literal_columns: &Vec>, - ) -> Result>, Error> { + ) -> Result>, Error> { let literal_values = typed_statement.literal_values(); if literal_values.is_empty() { debug!(target: MAPPER, @@ -643,7 +643,7 @@ where async fn transform_statement( &mut self, typed_statement: &TypeCheckedStatement<'_>, - encrypted_literals: &Vec>, + encrypted_literals: &Vec>, ) -> Result, Error> { // Convert literals to ast Expr let mut encrypted_expressions = vec![]; @@ -1042,7 +1042,7 @@ where session_id: Option, bind: &Bind, statement: &Statement, - ) -> Result>, Error> { + ) -> Result>, Error> { let plaintexts = bind.to_plaintext(&statement.param_columns, &statement.postgres_param_types)?; diff --git a/packages/cipherstash-proxy/src/postgresql/messages/bind.rs b/packages/cipherstash-proxy/src/postgresql/messages/bind.rs index a8dbf734..5446bd0d 100644 --- a/packages/cipherstash-proxy/src/postgresql/messages/bind.rs +++ b/packages/cipherstash-proxy/src/postgresql/messages/bind.rs @@ -8,7 +8,7 @@ use crate::postgresql::protocol::BytesMutReadString; use crate::{SIZE_I16, SIZE_I32}; use bytes::{Buf, BufMut, BytesMut}; use cipherstash_client::encryption::Plaintext; -use cipherstash_client::eql::EqlCiphertext; +use cipherstash_client::eql::EqlOutput; use postgres_types::Type; use std::fmt::{self, Display, Formatter}; use std::io::Cursor; @@ -81,7 +81,7 @@ impl Bind { Ok(plaintexts) } - pub fn rewrite(&mut self, encrypted: Vec>) -> Result<(), Error> { + pub fn rewrite(&mut self, encrypted: Vec>) -> Result<(), Error> { for (idx, ct) in encrypted.iter().enumerate() { if let Some(ct) = ct { let json = serde_json::to_value(ct)?; diff --git a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs b/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs index e512eb66..c1341f95 100644 --- a/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs +++ b/packages/cipherstash-proxy/src/postgresql/messages/data_row.rs @@ -5,7 +5,7 @@ use crate::{ postgresql::Column, }; use bytes::{Buf, BufMut, BytesMut}; -use cipherstash_client::eql::EqlCiphertext; +use cipherstash_client::eql::{EqlCiphertext, EQL_SCHEMA_VERSION}; use std::io::Cursor; use tracing::{debug, error}; @@ -191,7 +191,7 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext { let input = String::from_utf8_lossy(sliced).to_string(); let input = input.replace("\"\"", "\""); - match serde_json::from_str(&input) { + match eql_ciphertext_from_json(input.as_bytes()) { Ok(e) => return Ok(e), Err(err) => { debug!(target: DECRYPT, error = err.to_string()); @@ -221,7 +221,7 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext { let start = 12 + 1; let sliced = &bytes[start..]; - match serde_json::from_slice(sliced) { + match eql_ciphertext_from_json(sliced) { Ok(e) => { return Ok(e); } @@ -237,6 +237,64 @@ impl TryFrom<&mut DataColumn> for EqlCiphertext { } } +/// Deserialize an EQL ciphertext payload read from the database. +/// +/// Supports both the current EQL v2.x storage format (a tagged object +/// discriminated by `"k"`, e.g. `{"k":"ct",...}`) and the legacy pre-v2.x flat +/// format that predates the `cipherstash-client` 0.37 upgrade. Existing customer +/// databases may still hold values written in the legacy format, so the proxy +/// must continue to read them transparently. +fn eql_ciphertext_from_json(input: &[u8]) -> Result { + let value: serde_json::Value = serde_json::from_slice(input)?; + + // The current format always carries the `k` discriminator. Anything without + // it is a legacy payload and is remapped onto the current schema. + if value.get("k").is_some() { + serde_json::from_value(value) + } else { + serde_json::from_value(legacy_to_current(value)) + } +} + +/// Remap a legacy (pre-v2.x) EQL payload onto the current scalar storage shape. +/// +/// The legacy format stored the encrypted record under `c`, the identifier under +/// `i`, and index terms under `m` (bloom filter), `o` (block ORE), and `u` +/// (HMAC). The current scalar payload (`k = "ct"`) renames these to `bf`, `ob`, +/// and `hm` respectively. Decryption only requires the root ciphertext (`c`) and +/// identifier (`i`); index terms are carried over best-effort. Legacy structured +/// (JSON / STE-vec) payloads also retained a root `c`, so they decrypt correctly +/// through the same scalar mapping. +fn legacy_to_current(old: serde_json::Value) -> serde_json::Value { + use serde_json::Value; + + let mut new = serde_json::Map::new(); + new.insert("k".to_string(), Value::String("ct".to_string())); + new.insert( + "v".to_string(), + old.get("v") + .filter(|v| !v.is_null()) + .cloned() + .unwrap_or_else(|| Value::from(EQL_SCHEMA_VERSION)), + ); + + // Carry over a field from the legacy payload under a (possibly renamed) key, + // skipping nulls so optional terms stay absent rather than `null`. + let mut carry = |old_key: &str, new_key: &str| { + if let Some(v) = old.get(old_key).filter(|v| !v.is_null()) { + new.insert(new_key.to_string(), v.clone()); + } + }; + + carry("i", "i"); // identifier + carry("c", "c"); // encrypted record + carry("u", "hm"); // HMAC (exact match) + carry("m", "bf"); // bloom filter (LIKE / ILIKE) + carry("o", "ob"); // block ORE (ordering) + + Value::Object(new) +} + #[cfg(test)] mod tests { use super::DataRow; @@ -284,7 +342,7 @@ mod tests { assert_eq!( column_config[1].as_ref().unwrap().identifier, - encrypted[1].as_ref().unwrap().identifier + *encrypted[1].as_ref().unwrap().identifier() ); } @@ -333,7 +391,7 @@ mod tests { assert_eq!( column_config[0].as_ref().unwrap().identifier, - encrypted[0].as_ref().unwrap().identifier + *encrypted[0].as_ref().unwrap().identifier() ); } @@ -374,7 +432,7 @@ mod tests { assert_eq!( column_config[2].as_ref().unwrap().identifier, - encrypted[2].as_ref().unwrap().identifier + *encrypted[2].as_ref().unwrap().identifier() ); } diff --git a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs index 65dcaf0d..5fafddf5 100644 --- a/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs +++ b/packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs @@ -248,7 +248,9 @@ fn canonical_to_map(canonical: CanonicalEncryptionConfig) -> Result, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error>; + ) -> Result>, Error>; /// Decrypt values retrieved from the database async fn decrypt( diff --git a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs index 15e120d3..95cf08c4 100644 --- a/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs +++ b/packages/cipherstash-proxy/src/proxy/zerokms/zerokms.rs @@ -13,7 +13,7 @@ use cipherstash_client::{ encryption::{Plaintext, QueryOp}, eql::{ decrypt_eql, encrypt_eql, EqlCiphertext, EqlDecryptOpts, EqlEncryptOpts, EqlOperation, - PreparedPlaintext, + EqlOutput, PreparedPlaintext, }, schema::column::IndexType, }; @@ -157,7 +157,7 @@ impl EncryptionService for ZeroKms { keyset_id: Option, plaintexts: Vec>, columns: &[Option], - ) -> Result>, Error> { + ) -> Result>, Error> { debug!(target: ENCRYPT, msg="Encrypt", ?keyset_id, default_keyset_id = ?self.default_keyset_id); // A keyset is required if no default keyset has been configured @@ -216,7 +216,7 @@ impl EncryptionService for ZeroKms { // If no plaintexts to encrypt, return all None if prepared_plaintexts.is_empty() { - return Ok(vec![None; plaintexts.len()]); + return Ok((0..plaintexts.len()).map(|_| None).collect()); } // Use default opts since cipher is already initialized with the correct keyset @@ -231,9 +231,9 @@ impl EncryptionService for ZeroKms { debug!(target: ENCRYPT, msg="encrypt_eql completed", count = encrypted.len(), duration_ms = encrypt_duration.as_millis()); // Reconstruct the result vector with None values in the right places - let mut result: Vec> = vec![None; plaintexts.len()]; - for (idx, ciphertext) in indices.into_iter().zip(encrypted.into_iter()) { - result[idx] = Some(ciphertext); + let mut result: Vec> = (0..plaintexts.len()).map(|_| None).collect(); + for (idx, output) in indices.into_iter().zip(encrypted.into_iter()) { + result[idx] = Some(output); } Ok(result) diff --git a/vendor/stack-auth/.gitignore b/vendor/stack-auth/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/vendor/stack-auth/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/vendor/stack-auth/Cargo.lock b/vendor/stack-auth/Cargo.lock deleted file mode 100644 index 07fa5c2c..00000000 --- a/vendor/stack-auth/Cargo.lock +++ /dev/null @@ -1,4154 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" - -[[package]] -name = "aquamarine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" -dependencies = [ - "include_dir", - "itertools 0.10.5", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] - -[[package]] -name = "async-compression" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f" -dependencies = [ - "compression-codecs", - "compression-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" -dependencies = [ - "bytemuck", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-lc-rs" -version = "1.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" -dependencies = [ - "aws-lc-sys", - "untrusted 0.7.1", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" -dependencies = [ - "axum-core", - "bytes", - "form_urlencoded", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-core" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "backtrace-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" -dependencies = [ - "backtrace", -] - -[[package]] -name = "base32" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[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 = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "cached" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9718806c4a2fe9e8a56fd736f97b340dd10ed1be8ed733ed50449f351dc33cae" -dependencies = [ - "ahash", - "cached_proc_macro", - "cached_proc_macro_types", - "hashbrown 0.14.5", - "once_cell", - "thiserror 1.0.69", - "web-time", -] - -[[package]] -name = "cached_proc_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f42a145ed2d10dce2191e1dcf30cfccfea9026660e143662ba5eec4017d5daa" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" - -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.0", -] - -[[package]] -name = "chrono" -version = "0.4.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "cipherstash-config" -version = "0.34.1-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283fa04db19f9bf2cb2f09e8c1505a15560310bc50fdc066734072c616aa8ca9" -dependencies = [ - "bitflags", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "compression-codecs" -version = "0.4.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" -dependencies = [ - "brotli", - "compression-core", - "flate2", - "memchr", -] - -[[package]] -name = "compression-core" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" - -[[package]] -name = "const-hex" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "proptest", - "serde_core", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cts-common" -version = "0.34.1-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b26644e630f2e690194c6b61f5b613b768061750f2060cf4db73ddb8058d284" -dependencies = [ - "arrayvec", - "base32", - "cached", - "chrono", - "derive_more", - "either", - "miette", - "nom", - "regex", - "serde", - "serde_json", - "thiserror 1.0.69", - "tracing", - "url", - "utoipa", - "uuid", - "vitaminc", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dummy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac124e13ae9aa56acc4241f8c8207501d93afdd8d8e62f0c1f2e12f6508c65" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fake" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d391ba4af7f1d93f01fcf7b2f29e2bc9348e109dfdbf4dcbdc51dfa38dab0b6" -dependencies = [ - "deunicode", - "dummy", - "rand 0.8.6", - "uuid", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[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 = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "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 = "gethostname" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30" -dependencies = [ - "rustix 0.38.44", - "windows-targets 0.52.6", -] - -[[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", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "rand_core 0.10.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand 0.9.2", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand 0.9.2", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -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 = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.2", - "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.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is-docker" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" -dependencies = [ - "once_cell", -] - -[[package]] -name = "is-wsl" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" -dependencies = [ - "is-docker", - "once_cell", -] - -[[package]] -name = "is_ci" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.180" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" - -[[package]] -name = "libredox" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" -dependencies = [ - "bitflags", - "libc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[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.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "backtrace", - "backtrace-ext", - "cfg-if", - "miette-derive", - "owo-colors", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", - "textwrap", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "mocktail" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053f7ba52863e22dfd2970075bbc69c4224ca6ae03896a5f69a0d5982deb5e0a" -dependencies = [ - "bytes", - "futures", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "prost", - "rand 0.9.2", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "moka" -version = "0.12.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "open" -version = "5.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" -dependencies = [ - "is-wsl", - "libc", - "pathdiff", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "owo-colors" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" - -[[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 = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64", - "serde_core", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -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 = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" -dependencies = [ - "bitflags", - "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "unarray", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.6.2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.2", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" -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 = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.0", -] - -[[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64", - "bytes", - "futures-core", - "futures-util", - "hickory-resolver", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "once_cell", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[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 = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "aws-lc-rs", - "once_cell", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "time", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "stack-auth" -version = "0.34.1-alpha.4" -dependencies = [ - "aquamarine", - "axum", - "cts-common", - "jsonwebtoken", - "miette", - "mocktail", - "open", - "reqwest", - "serde", - "serde_json", - "stack-profile", - "tempfile", - "thiserror 1.0.69", - "tokio", - "tracing", - "tracing-subscriber", - "url", - "uuid", - "vitaminc", - "vitaminc-protected", - "zeroize", - "zerokms-protocol", -] - -[[package]] -name = "stack-profile" -version = "0.34.1-alpha.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd61bc4129d2258ec1ba89d742558308560fc0f585f9c24d478685def8efd14" -dependencies = [ - "dirs", - "gethostname", - "serde", - "serde_json", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[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 = "supports-color" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" -dependencies = [ - "is_ci", -] - -[[package]] -name = "supports-hyperlinks" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" - -[[package]] -name = "supports-unicode" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" - -[[package]] -name = "syn" -version = "2.0.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminal_size" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" -dependencies = [ - "rustix 1.1.3", - "windows-sys 0.60.2", -] - -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "unicode-linebreak", - "unicode-width 0.2.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "async-compression", - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "iri-string", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", -] - -[[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-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[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.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-ident" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[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 = "utoipa" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" -dependencies = [ - "indexmap", - "serde", - "serde_json", - "utoipa-gen", -] - -[[package]] -name = "utoipa-gen" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "url", - "uuid", -] - -[[package]] -name = "uuid" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" -dependencies = [ - "atomic", - "getrandom 0.3.4", - "js-sys", - "md-5", - "rand 0.9.2", - "serde_core", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "validator" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" -dependencies = [ - "idna", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" -dependencies = [ - "darling", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vitaminc" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c8b739a2cb1e528e77a69267728532f52d2d5ce18ae2839e26c797859fe9015" -dependencies = [ - "vitaminc-aead", - "vitaminc-encrypt", - "vitaminc-protected", - "vitaminc-random", - "vitaminc-traits", -] - -[[package]] -name = "vitaminc-aead" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c29cef4d4b0d018c4223d366017d2a9756012acf76e25011aaca877f3c74904" -dependencies = [ - "bytes", - "serde", - "vitaminc-protected", - "vitaminc-random", - "zeroize", -] - -[[package]] -name = "vitaminc-encrypt" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e3869aaf60ebb95ccbdfcf003985132325b4d1ac6f5d945ad2fbb9149afd3a" -dependencies = [ - "aws-lc-rs", - "vitaminc-aead", - "vitaminc-protected", - "vitaminc-random", - "zeroize", -] - -[[package]] -name = "vitaminc-protected" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af693c39d3cd1c818ef6267539433c6ceca87840b12d24124adbc9c8ecba1709" -dependencies = [ - "bitvec", - "digest", - "serde", - "serde_bytes", - "subtle", - "vitaminc-protected-derive", - "zeroize", -] - -[[package]] -name = "vitaminc-protected-derive" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74520596b66eec546ef18d5376f6f18cdaf874caca9fa39e03eb12f9abb76fa" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "vitaminc-random" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9de431cb93359d293ec7e70d05d87117a57f34bfc5bc94f040b81d4dd1afd6" -dependencies = [ - "rand 0.10.0", - "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random-derives", - "zeroize", -] - -[[package]] -name = "vitaminc-random-derives" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49d33ac4682235551d25c874525c20e03d4c863b39f556391f52f7a2083bfbdf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "vitaminc-traits" -version = "0.1.0-pre4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c25a9e51d24c3befddd71e907dd4ae9f21cfbaae065fb0ef5202e5d21cd198d0" -dependencies = [ - "anyhow", - "bytes", - "rmp-serde", - "serde", - "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random", - "zeroize", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[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.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" -dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[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", -] - -[[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", -] - -[[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.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[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.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[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_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[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_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerokms-protocol" -version = "0.12.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a2f045e2ee975a3d448419245c4621ea8844d2a004c63a96277181dc7cf8483" -dependencies = [ - "base64", - "cipherstash-config", - "const-hex", - "cts-common", - "fake", - "opaque-debug", - "rand 0.8.6", - "serde", - "static_assertions", - "thiserror 1.0.69", - "utoipa", - "uuid", - "validator", - "zeroize", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/vendor/stack-auth/Cargo.toml b/vendor/stack-auth/Cargo.toml deleted file mode 100644 index 77f70abf..00000000 --- a/vendor/stack-auth/Cargo.toml +++ /dev/null @@ -1,166 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -name = "stack-auth" -version = "0.34.1-alpha.4" -authors = [ - "Dan Draper ", - "Drew Thomas ", - "Fiona McCawley ", - "James Sadler ", - "Kate Andrews ", - "Lindsay Holmwood ", - "Paul Hawkins ", - "Robin Howard ", - "Toby Hede ", - "Yuji Yokoo ", -] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = "Authentication library for CipherStash services" -homepage = "https://cipherstash.com" -readme = "README.md" -license-file = "LICENSE" -repository = "https://github.com/cipherstash/cipherstash-suite" - -[features] -test-utils = [] - -[lib] -name = "stack_auth" -path = "src/lib.rs" - -[[example]] -name = "auto_strategy" -path = "examples/auto_strategy.rs" - -[[example]] -name = "device_code" -path = "examples/device_code.rs" -required-features = ["test-utils"] - -[dependencies.aquamarine] -version = "0.6" - -[dependencies.cts-common] -version = "0.34.1-alpha.4" -default-features = false - -[dependencies.jsonwebtoken] -version = "9.3.1" - -[dependencies.miette] -version = "7.5.0" -features = ["fancy"] - -[dependencies.open] -version = "5.3.2" - -[dependencies.reqwest] -version = "0.13" -features = [ - "brotli", - "gzip", - "json", - "rustls", - "hickory-dns", - "stream", - "form", - "query", -] -default-features = false - -[dependencies.serde] -version = "1.0" -features = ["derive"] - -[dependencies.serde_json] -version = "1.0.132" - -[dependencies.stack-profile] -version = "0.34.1-alpha.4" - -[dependencies.thiserror] -version = "1.0.56" - -[dependencies.tokio] -version = "1.47.1" -features = ["full"] - -[dependencies.tracing] -version = "0.1" -features = ["log"] - -[dependencies.url] -version = "2.5.4" -features = ["serde"] - -[dependencies.uuid] -version = "1.8" -features = [ - "v4", - "v5", - "serde", -] - -[dependencies.vitaminc] -version = "0.1.0-pre4.2" -features = [ - "random", - "protected", - "encrypt", - "protected", -] - -[dependencies.vitaminc-protected] -version = "0.1.0-pre4.2" - -[dependencies.zeroize] -version = "1.8.1" -features = ["derive"] - -[dependencies.zerokms-protocol] -version = "0.12.9" - -[dev-dependencies.axum] -version = "0.8" - -[dev-dependencies.cts-common] -version = "0.34.1-alpha.4" -default-features = false - -[dev-dependencies.mocktail] -version = "0.3.0" - -[dev-dependencies.tempfile] -version = "3.21.0" - -[dev-dependencies.tokio] -version = "1.47.1" -features = [ - "full", - "test-util", -] - -[dev-dependencies.tracing-subscriber] -version = "0.3" -features = [ - "ansi", - "json", - "env-filter", - "std", -] diff --git a/vendor/stack-auth/LICENSE b/vendor/stack-auth/LICENSE deleted file mode 100644 index 2cbd67a6..00000000 --- a/vendor/stack-auth/LICENSE +++ /dev/null @@ -1,96 +0,0 @@ -# PolyForm Internal Use License 1.0.0 - - - -## Acceptance - -In order to get any license under these terms, you must agree -to them as both strict obligations and conditions to all -your licenses. - -## Copyright License - -The licensor grants you a copyright license for the software -to do everything you might do with the software that would -otherwise infringe the licensor's copyright in it for any -permitted purpose. However, you may only make changes or -new works based on the software according to [Changes and New -Works License](#changes-and-new-works-license), and you may -not distribute the software. - -## Changes and New Works License - -The licensor grants you an additional copyright license to -make changes and new works based on the software for any -permitted purpose. - -## Patent License - -The licensor grants you a patent license for the software that -covers patent claims the licensor can license, or becomes able -to license, that you would infringe by using the software. - -## Fair Use - -You may have "fair use" rights for the software under the -law. These terms do not limit them. - -## Internal Business Use - -Use of the software for the internal business operations of -you and your company is use for a permitted purpose. - -## No Other Rights - -These terms do not allow you to sublicense or transfer any of -your licenses to anyone else, or prevent the licensor from -granting licenses to anyone else. These terms do not imply -any other licenses. - -## Patent Defense - -If you make any written claim that the software infringes or -contributes to infringement of any patent, your patent license -for the software granted under these terms ends immediately. If -your company makes such a claim, your patent license ends -immediately for work on behalf of your company. - -## Violations - -The first time you are notified in writing that you have -violated any of these terms, or done anything with the software -not covered by your licenses, your licenses can nonetheless -continue if you come into full compliance with these terms, -and take practical steps to correct past violations, within -32 days of receiving notice. Otherwise, all your licenses -end immediately. - -## No Liability - -***As far as the law allows, the software comes as is, without -any warranty or condition, and the licensor will not be liable -to you for any damages arising out of these terms or the use -or nature of the software, under any kind of legal claim.*** - -## Definitions - -The **licensor** is the individual or entity offering these -terms, and the **software** is the software the licensor makes -available under these terms. - -**You** refers to the individual or entity agreeing to these -terms. - -**Your company** is any legal entity, sole proprietorship, -or other kind of organization that you work for, plus all -organizations that have control over, are under the control of, -or are under common control with that organization. **Control** -means ownership of substantially all the assets of an entity, -or the power to direct its management and policies by vote, -contract, or otherwise. Control can be direct or indirect. - -**Your licenses** are all the licenses granted to you for the -software under these terms. - -**Use** means anything you do with the software requiring one -of your licenses. diff --git a/vendor/stack-auth/README.md b/vendor/stack-auth/README.md deleted file mode 100644 index d03c569d..00000000 --- a/vendor/stack-auth/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# stack-auth - -[![Crates.io Version](https://img.shields.io/crates/v/stack-auth?style=for-the-badge)](https://crates.io/crates/stack-auth) -[![docs.rs](https://img.shields.io/docsrs/stack-auth?style=for-the-badge)](https://docs.rs/stack-auth/) -[![Built by CipherStash](https://raw.githubusercontent.com/cipherstash/meta/refs/heads/main/csbadge.svg)](https://cipherstash.com) - - [Website](https://cipherstash.com) | [Docs](https://cipherstash.com/docs) | [Discord](https://discord.com/invite/5qwXUFb6PB) - -Authentication strategies for [CipherStash](https://cipherstash.com) services. - -All strategies implement the [`AuthStrategy`] trait, which provides a single -[`get_token`](AuthStrategy::get_token) method that returns a valid -[`ServiceToken`]. Token caching and refresh are handled automatically. - -## Strategies - -| Strategy | Use case | Credentials | -|---|---|---| -| [`AutoStrategy`] | Recommended default — detects credentials automatically | `CS_CLIENT_ACCESS_KEY` + `CS_WORKSPACE_CRN`, or `~/.cipherstash/auth.json` | -| [`AccessKeyStrategy`] | Service-to-service / CI | Static access key + region | -| [`OAuthStrategy`] | Long-lived sessions with refresh | OAuth token (from device code flow or disk) | -| [`DeviceCodeStrategy`] | CLI login ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) | User authorizes in browser | -| `StaticTokenStrategy` | Tests only (`test-utils` feature) | Pre-obtained token used as-is | - -## Quick start - -For most applications, [`AutoStrategy`] is the simplest way to get started: - -```no_run -use stack_auth::AutoStrategy; - -# async fn run() -> Result<(), Box> { -let strategy = AutoStrategy::detect()?; -// That's it — get_token() handles the rest. -# Ok(()) -# } -``` - -For service-to-service authentication with an access key: - -```no_run -use stack_auth::AccessKeyStrategy; -use cts_common::Region; - -# fn run() -> Result<(), Box> { -let region = Region::aws("ap-southeast-2")?; -let key = "CSAKkeyId.keySecret".parse()?; -let strategy = AccessKeyStrategy::new(region, key)?; -# Ok(()) -# } -``` - -## Security - -Sensitive values ([`SecretToken`]) are automatically zeroized when dropped -and are masked in [`Debug`](std::fmt::Debug) output to prevent accidental -leaks in logs. - -## Token refresh - -All strategies that cache tokens ([`AccessKeyStrategy`], [`OAuthStrategy`], -[`AutoStrategy`]) share the same internal refresh engine. See the -[`AuthStrategy`] trait docs for a full description of the concurrency model -and flow diagram. diff --git a/vendor/stack-auth/examples/auto_strategy.rs b/vendor/stack-auth/examples/auto_strategy.rs deleted file mode 100644 index 0b74df06..00000000 --- a/vendor/stack-auth/examples/auto_strategy.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Demonstrates automatic credential detection with [`AutoStrategy`]. -//! -//! `AutoStrategy` picks the best available authentication method without -//! requiring the caller to choose one explicitly. It checks for credentials -//! in the following order: -//! -//! 1. **Access key** – if `CS_CLIENT_ACCESS_KEY` is set along with -//! `CS_WORKSPACE_CRN`, an [`AccessKeyStrategy`] is used. -//! 2. **OAuth** – if a token store file exists at `~/.cipherstash/auth.json` -//! (written by `stash login`), an [`OAuthStrategy`] is used. -//! 3. If neither is available, an error is returned. -//! -//! # Running the example -//! -//! With an access key: -//! -//! ```sh -//! CS_CLIENT_ACCESS_KEY= CS_WORKSPACE_CRN= cargo run --example auto_strategy -//! ``` -//! -//! Or after authenticating via the CLI: -//! -//! ```sh -//! stash login -//! cargo run --example auto_strategy -//! ``` - -use stack_auth::{AuthStrategy, AutoStrategy}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - - // AutoStrategy detects credentials automatically: - // - // 1. CS_CLIENT_ACCESS_KEY env var → AccessKeyStrategy - // 2. ~/.cipherstash/auth.json file → OAuthStrategy - // 3. Neither → error - let strategy = AutoStrategy::detect()?; - - match &strategy { - AutoStrategy::AccessKey(_) => println!("Using access key authentication"), - AutoStrategy::OAuth(_) => println!("Using OAuth authentication"), - } - - // Obtain a token — refresh happens automatically when needed. - let token = (&strategy).get_token().await?; - println!("Subject: {}", token.subject()?); - println!("Workspace: {}", token.workspace_id()?); - println!("Issuer: {}", token.issuer()?); - - Ok(()) -} diff --git a/vendor/stack-auth/examples/device_code.rs b/vendor/stack-auth/examples/device_code.rs deleted file mode 100644 index 1fd1e727..00000000 --- a/vendor/stack-auth/examples/device_code.rs +++ /dev/null @@ -1,32 +0,0 @@ -use cts_common::Region; -use stack_auth::DeviceCodeStrategy; - -#[tokio::main] -async fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - - let region = Region::aws("ap-southeast-2")?; - let strategy = DeviceCodeStrategy::builder(region, "cli") - .base_url("http://localhost:3001".parse()?) - .build()?; - - // Step 1: Begin the device code flow - let pending = strategy.begin().await?; - - // Step 2: Display the code and open the browser (caller controls this) - println!("Your code is: {}", pending.user_code()); - println!("Visit: {}", pending.verification_uri_complete()); - - if !pending.open_in_browser() { - eprintln!("Could not open browser — please visit the URL above manually."); - } - - // Step 3: Poll until the user authorizes - let token = pending.poll_for_token().await?; - - println!("Token type: {}", token.token_type()); - println!("Expires in: {}s", token.expires_in()); - println!("Access token: {:?}", token.access_token()); - - Ok(()) -} diff --git a/vendor/stack-auth/src/access_key.rs b/vendor/stack-auth/src/access_key.rs deleted file mode 100644 index cef3285e..00000000 --- a/vendor/stack-auth/src/access_key.rs +++ /dev/null @@ -1,149 +0,0 @@ -use std::str::FromStr; - -use crate::SecretToken; -use vitaminc::protected::OpaqueDebug; - -/// The prefix that all CipherStash access keys start with. -const ACCESS_KEY_PREFIX: &str = "CSAK"; - -/// A CipherStash access key. -/// -/// Access keys have the format `CSAK.` and are used to -/// authenticate with the CipherStash Token Service (CTS). -/// -/// The inner value is stored as a [`SecretToken`], so it is zeroized on drop -/// and hidden from debug output. -/// -/// # Parsing -/// -/// ``` -/// use stack_auth::AccessKey; -/// -/// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap(); -/// ``` -/// -/// Invalid keys are rejected: -/// -/// ``` -/// use stack_auth::AccessKey; -/// -/// assert!("not-a-valid-key".parse::().is_err()); -/// assert!("CSAKmissing-dot".parse::().is_err()); -/// assert!("CSAK.no-key-id".parse::().is_err()); -/// assert!("CSAKno-secret.".parse::().is_err()); -/// ``` -#[derive(OpaqueDebug)] -pub struct AccessKey(SecretToken); - -impl AccessKey { - /// Expose the underlying [`SecretToken`]. - pub(crate) fn into_secret_token(self) -> SecretToken { - self.0 - } -} - -// NOTE: The format validation here mirrors `UnverifiedAccessKey::new()` in -// `cts-domain`. If the `CSAK.` format changes, both -// locations must be updated. -impl FromStr for AccessKey { - type Err = InvalidAccessKey; - - fn from_str(s: &str) -> Result { - let rest = s - .strip_prefix(ACCESS_KEY_PREFIX) - .ok_or(InvalidAccessKey::MissingPrefix)?; - - let (id, secret) = rest.split_once('.').ok_or(InvalidAccessKey::MissingDot)?; - - if id.is_empty() { - return Err(InvalidAccessKey::EmptyKeyId); - } - if secret.is_empty() { - return Err(InvalidAccessKey::EmptySecret); - } - - Ok(Self(SecretToken::new(s))) - } -} - -/// Error returned when parsing an invalid access key string. -#[derive(Debug, thiserror::Error)] -pub enum InvalidAccessKey { - /// The string does not start with the `CSAK` prefix. - #[error("access key must start with \"{ACCESS_KEY_PREFIX}\"")] - MissingPrefix, - /// No `.` separator found between key ID and secret. - #[error("access key must contain a \".\" separator")] - MissingDot, - /// The key ID portion (before the `.`) is empty. - #[error("access key ID must not be empty")] - EmptyKeyId, - /// The secret portion (after the `.`) is empty. - #[error("access key secret must not be empty")] - EmptySecret, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn valid_key() { - let key: AccessKey = - "CSAKT4ZMT2AUPXI7TCD2.ZAQRW2BWXP3Z6SHR4YG2TP3N35LLU46ZAWLR3BL5WUR4IIGA" - .parse() - .unwrap(); - assert_eq!( - key.0.as_str(), - "CSAKT4ZMT2AUPXI7TCD2.ZAQRW2BWXP3Z6SHR4YG2TP3N35LLU46ZAWLR3BL5WUR4IIGA" - ); - } - - #[test] - fn missing_prefix() { - let err = "key_id.key_secret".parse::().unwrap_err(); - assert!(matches!(err, InvalidAccessKey::MissingPrefix)); - } - - #[test] - fn missing_dot() { - let err = "CSAKnodot".parse::().unwrap_err(); - assert!(matches!(err, InvalidAccessKey::MissingDot)); - } - - #[test] - fn empty_key_id() { - let err = "CSAK.secret".parse::().unwrap_err(); - assert!(matches!(err, InvalidAccessKey::EmptyKeyId)); - } - - #[test] - fn empty_secret() { - let err = "CSAKid.".parse::().unwrap_err(); - assert!(matches!(err, InvalidAccessKey::EmptySecret)); - } - - #[test] - fn empty_string() { - let err = "".parse::().unwrap_err(); - assert!(matches!(err, InvalidAccessKey::MissingPrefix)); - } - - #[test] - fn into_secret_token() { - let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap(); - let secret = key.into_secret_token(); - assert_eq!(secret.as_str(), "CSAKmyKeyId.myKeySecret"); - } - - #[test] - fn debug_does_not_leak() { - let key: AccessKey = "CSAKid.secret".parse().unwrap(); - let debug = format!("{key:?}"); - assert!(!debug.contains("secret")); - assert!( - debug.contains("AccessKey") && debug.contains("***"), - "debug should hide secret: {debug}" - ); - } -} diff --git a/vendor/stack-auth/src/access_key_refresher.rs b/vendor/stack-auth/src/access_key_refresher.rs deleted file mode 100644 index 396babcc..00000000 --- a/vendor/stack-auth/src/access_key_refresher.rs +++ /dev/null @@ -1,698 +0,0 @@ -use std::sync::Arc; - -use url::Url; - -use crate::refresher::Refresher; -use crate::{http_client, AuthError, SecretToken, Token}; - -/// A [`Refresher`] that uses a static access key to authenticate. -/// -/// Unlike OAuth, the access key never changes — `try_credential` always returns -/// `Some(())` and `restore` is a no-op. This means `AutoRefresh` can perform -/// initial authentication on the first `get_token()` call (cold start). -pub(crate) struct AccessKeyRefresher { - access_key: SecretToken, - base_url: Url, - audience: Option, - http_client: Arc, -} - -impl AccessKeyRefresher { - pub(crate) fn new(access_key: SecretToken, base_url: Url, audience: Option) -> Self { - Self { - access_key, - base_url, - audience, - http_client: Arc::new(http_client()), - } - } -} - -impl Refresher for AccessKeyRefresher { - type Credential = (); - - fn save(&self, _token: &Token) { - // Access key tokens are ephemeral — no persistence needed. - } - - fn try_credential(&self, _token: Option<&mut Token>) -> Option { - Some(()) - } - - fn restore(&self, _token: &mut Token, _credential: Self::Credential) { - // Nothing to restore — the access key is always available. - } - - async fn refresh(&self, _credential: &Self::Credential) -> Result { - let url = self.base_url.join("api/authorise")?; - - tracing::debug!(url = %url, "authenticating with access key"); - - let resp = self - .http_client - .post(url) - .json(&AuthoriseRequest { - access_key: self.access_key.as_str(), - audience: self.audience.as_deref(), - }) - .send() - .await?; - - if !resp.status().is_success() { - let status = resp.status(); - let body = resp.text().await.unwrap_or_default(); - tracing::debug!(%status, %body, "access key auth failed"); - return Err(AuthError::Server(format!("{status}: {body}"))); - } - - let auth_resp: AuthoriseResponse = resp.json().await?; - - Ok(Token { - access_token: auth_resp.access_token, - token_type: "Bearer".to_string(), - // CTS `/api/authorise` returns `expiry` as an ABSOLUTE Unix epoch (it is - // the JWT `exp` claim), NOT a relative duration. The previous `now + expiry` - // pushed the local expiry decades into the future, so `AutoRefresh` never - // considered the token expired and never refreshed it — the token then - // silently died at its real (~15 min) `exp` and every request failed until - // the process restarted. Use the value as-is. See CIP-3233. - expires_at: auth_resp.expiry, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - }) - } -} - -#[derive(serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct AuthoriseRequest<'a> { - access_key: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - audience: Option<&'a str>, -} - -#[derive(serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct AuthoriseResponse { - access_token: SecretToken, - expiry: u64, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::auto_refresh::{AutoRefresh, AutoRefreshError}; - use mocktail::prelude::*; - use std::sync::Arc; - use std::time::{SystemTime, UNIX_EPOCH}; - - /// Build a mock `/api/authorise` response. CTS returns `expiry` as an - /// ABSOLUTE Unix epoch (the JWT `exp` claim), so model that faithfully: the - /// token is valid for `expires_in_secs` from now. - fn auth_response_json(access: &str, expires_in_secs: u64) -> serde_json::Value { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - serde_json::json!({ - "accessToken": access, - "expiry": now + expires_in_secs - }) - } - - async fn start_server(mocks: MockSet) -> MockServer { - let server = MockServer::new_http("access-key-refresher-test").with_mocks(mocks); - server.start().await.unwrap(); - server - } - - fn make_access_key_strategy(server: &MockServer) -> AutoRefresh { - let refresher = AccessKeyRefresher::new( - SecretToken::new("test-access-key"), - server.url(""), - Some("test-audience".to_string()), - ); - AutoRefresh::new(refresher) - } - - fn make_expired_token(access: &str) -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - Token { - access_token: SecretToken::new(access), - token_type: "Bearer".to_string(), - expires_at: now, // already expired - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - } - } - - // ---- Regression: CTS `expiry` is an absolute epoch (CIP-3233) ---- - - /// CTS `/api/authorise` returns `expiry` as an ABSOLUTE Unix epoch (the JWT - /// `exp` claim), not a relative duration. The refresher must use it as-is. - /// - /// Pre-fix (`expires_at = now + expiry`), this token's `expires_at` lands - /// ~decades in the future, so `is_expired()` is never true — the token never - /// refreshes and silently dies at its real ~15-minute `exp`. The assertion - /// below fails under the pre-fix arithmetic (`expires_in()` ≈ 1.7e9) and - /// passes with the fix (`expires_in()` ≈ 900). - #[tokio::test] - async fn access_key_expiry_is_absolute_epoch_not_relative() { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let absolute_expiry = now + 900; // a 15-minute token, as an absolute epoch - - let mut mocks = MockSet::new(); - mocks.mock(move |when, then| { - when.post().path("/api/authorise"); - then.json(serde_json::json!({ - "accessToken": "tok", - "expiry": absolute_expiry - })); - }); - let server = start_server(mocks).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("CSAKid.secret"), server.url(""), None); - let token = refresher.refresh(&()).await.unwrap(); - - assert!( - token.expires_in() <= 1000, - "expires_in should be ~900s (absolute `expiry` used as-is); got {} \ - — pre-fix `now + expiry` yields ~1.7e9", - token.expires_in() - ); - assert!( - !token.is_expired(), - "a fresh 15-minute token must not be reported as already expired" - ); - } - - // ---- Initial auth tests ---- - - #[tokio::test] - async fn test_initial_auth_no_cached_token() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("new-token", 3600)); - }); - let server = start_server(mocks).await; - let strategy = make_access_key_strategy(&server); - - let token = strategy.get_token().await.unwrap(); - - assert_eq!(token.as_str(), "new-token"); - } - - #[tokio::test] - async fn test_caches_token_after_initial_auth() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("new-token", 3600)); - }); - let server = start_server(mocks).await; - let strategy = make_access_key_strategy(&server); - - let token1 = strategy.get_token().await.unwrap(); - assert_eq!(token1.as_str(), "new-token"); - - // Replace mock — second call should use cached token. - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/api/authorise"); - then.internal_server_error() - .json(serde_json::json!({"error": "should not be called"})); - }); - - let token2 = strategy.get_token().await.unwrap(); - assert_eq!(token2.as_str(), "new-token"); - } - - // ---- Refresh on expiry tests ---- - - #[tokio::test] - async fn test_re_authenticates_on_expiry() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("refreshed-token", 3600)); - }); - let server = start_server(mocks).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), server.url(""), None); - let strategy = AutoRefresh::with_token(refresher, make_expired_token("old-token")); - - let token = strategy.get_token().await.unwrap(); - - assert_eq!(token.as_str(), "refreshed-token"); - } - - // ---- Error handling tests ---- - - #[tokio::test] - async fn test_initial_auth_failure() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.unauthorized() - .json(serde_json::json!({"error": "invalid key"})); - }); - let server = start_server(mocks).await; - let strategy = make_access_key_strategy(&server); - - let err = strategy.get_token().await.unwrap_err(); - - assert!(matches!(err, AutoRefreshError::Auth(_))); - } - - #[tokio::test] - async fn test_refresh_failure_returns_expired() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.unauthorized() - .json(serde_json::json!({"error": "invalid key"})); - }); - let server = start_server(mocks).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), server.url(""), None); - let strategy = AutoRefresh::with_token(refresher, make_expired_token("old-token")); - - let err = strategy.get_token().await.unwrap_err(); - - assert!(matches!(err, AutoRefreshError::Expired)); - } - - // ---- Cascade prevention tests ---- - - #[tokio::test] - async fn test_concurrent_initial_auth_only_one_http_call() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("new-token", 3600)); - }); - let server = start_server(mocks).await; - let strategy = Arc::new(make_access_key_strategy(&server)); - - let s1 = Arc::clone(&strategy); - let handle_a = tokio::spawn(async move { s1.get_token().await.unwrap() }); - - let s2 = Arc::clone(&strategy); - let handle_b = tokio::spawn(async move { s2.get_token().await.unwrap() }); - - let (result_a, result_b) = tokio::join!(handle_a, handle_b); - let token_a = result_a.unwrap(); - let token_b = result_b.unwrap(); - - assert_eq!(token_a.as_str(), "new-token"); - assert_eq!(token_b.as_str(), "new-token"); - } - - #[tokio::test] - async fn test_concurrent_access_expired_token() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("refreshed-token", 3600)); - }); - let server = start_server(mocks).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), server.url(""), None); - let strategy = Arc::new(AutoRefresh::with_token( - refresher, - make_expired_token("old-token"), - )); - - let s1 = Arc::clone(&strategy); - let handle_a = tokio::spawn(async move { s1.get_token().await.unwrap() }); - - let s2 = Arc::clone(&strategy); - let handle_b = tokio::spawn(async move { s2.get_token().await.unwrap() }); - - let (result_a, result_b) = tokio::join!(handle_a, handle_b); - let token_a = result_a.unwrap(); - let token_b = result_b.unwrap(); - - assert_eq!(token_a.as_str(), "refreshed-token"); - assert_eq!(token_b.as_str(), "refreshed-token"); - } - - // ---- Concurrent access: expiring but usable ---- - - #[tokio::test] - async fn test_concurrent_access_expiring_but_usable() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/api/authorise"); - then.json(auth_response_json("refreshed-token", 3600)); - }); - let server = start_server(mocks).await; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let expiring_token = Token { - access_token: SecretToken::new("still-usable"), - token_type: "Bearer".to_string(), - expires_at: now + 30, // is_expired() = true (within 90s), is_usable() = true - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - }; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), server.url(""), None); - let strategy = Arc::new(AutoRefresh::with_token(refresher, expiring_token)); - - let s1 = Arc::clone(&strategy); - let handle_a = tokio::spawn(async move { s1.get_token().await.unwrap() }); - - let s2 = Arc::clone(&strategy); - let handle_b = tokio::spawn(async move { s2.get_token().await.unwrap() }); - - let (result_a, result_b) = tokio::join!(handle_a, handle_b); - let token_a = result_a.unwrap(); - let token_b = result_b.unwrap(); - - // Both should succeed with either old or refreshed token. - assert!( - token_a.as_str() == "still-usable" || token_a.as_str() == "refreshed-token", - "unexpected token_a: {}", - token_a.as_str() - ); - assert!( - token_b.as_str() == "still-usable" || token_b.as_str() == "refreshed-token", - "unexpected token_b: {}", - token_b.as_str() - ); - } - - // ---- Stress tests ---- - - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::time::{Duration, Instant}; - - #[derive(Clone)] - struct CountingState { - total: Arc, - current: Arc, - peak: Arc, - } - - impl CountingState { - fn new() -> Self { - Self { - total: Arc::new(AtomicUsize::new(0)), - current: Arc::new(AtomicUsize::new(0)), - peak: Arc::new(AtomicUsize::new(0)), - } - } - - fn enter(&self) { - self.total.fetch_add(1, Ordering::SeqCst); - let prev = self.current.fetch_add(1, Ordering::SeqCst); - self.peak.fetch_max(prev + 1, Ordering::SeqCst); - } - - fn exit(&self) { - self.current.fetch_sub(1, Ordering::SeqCst); - } - - fn peak(&self) -> usize { - self.peak.load(Ordering::SeqCst) - } - - fn total(&self) -> usize { - self.total.load(Ordering::SeqCst) - } - } - - #[derive(Clone)] - struct DelayedAuthState { - counting: CountingState, - delay: Duration, - } - - async fn delayed_auth_handler( - axum::extract::State(state): axum::extract::State, - ) -> axum::Json { - state.counting.enter(); - tokio::time::sleep(state.delay).await; - state.counting.exit(); - // CTS returns `expiry` as an absolute epoch (JWT `exp`); model a token - // valid for 1 hour from now. - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - axum::Json(serde_json::json!({ - "accessToken": "refreshed-token", - "expiry": now + 3600 - })) - } - - async fn start_axum_server(state: DelayedAuthState) -> (Url, CountingState) { - let counting = state.counting.clone(); - let app = axum::Router::new() - .route("/api/authorise", axum::routing::post(delayed_auth_handler)) - .with_state(state); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - let base_url = Url::parse(&format!("http://{addr}")).unwrap(); - (base_url, counting) - } - - const CONCURRENCY: usize = 50; - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn test_stress_initial_auth() { - let state = DelayedAuthState { - counting: CountingState::new(), - delay: Duration::from_millis(200), - }; - let (base_url, stats) = start_axum_server(state).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), base_url, None); - let strategy = Arc::new(AutoRefresh::new(refresher)); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for token in &results { - assert_eq!(token.as_str(), "refreshed-token"); - } - - assert!( - elapsed < Duration::from_millis(600), - "expected < 600ms, got {:?}", - elapsed - ); - assert_eq!(stats.total(), 1, "only one auth request should be made"); - assert_eq!(stats.peak(), 1, "peak concurrency to auth endpoint"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn test_stress_cached_token() { - let state = DelayedAuthState { - counting: CountingState::new(), - delay: Duration::from_millis(500), - }; - let (base_url, stats) = start_axum_server(state).await; - - // Pre-authenticate. - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), base_url, None); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let token = Token { - access_token: SecretToken::new("cached-token"), - token_type: "Bearer".to_string(), - expires_at: now + 3600, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - }; - let strategy = Arc::new(AutoRefresh::with_token(refresher, token)); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for token in &results { - assert_eq!(token.as_str(), "cached-token"); - } - - assert!( - elapsed < Duration::from_millis(200), - "expected < 200ms for cached tokens, got {:?}", - elapsed - ); - assert_eq!(stats.total(), 0, "no auth requests should be made"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn test_stress_expiring_but_usable_non_blocking() { - let state = DelayedAuthState { - counting: CountingState::new(), - delay: Duration::from_millis(500), - }; - let (base_url, stats) = start_axum_server(state).await; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let expiring_token = Token { - access_token: SecretToken::new("still-usable"), - token_type: "Bearer".to_string(), - expires_at: now + 30, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - }; - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), base_url, None); - let strategy = Arc::new(AutoRefresh::with_token(refresher, expiring_token)); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { - let call_start = Instant::now(); - let token = s.get_token().await.unwrap(); - (token, call_start.elapsed()) - })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let _elapsed = start.elapsed(); - - for (token, _) in &results { - assert!( - token.as_str() == "still-usable" || token.as_str() == "refreshed-token", - "unexpected token: {}", - token.as_str() - ); - } - - // At least N-1 callers should be fast (non-blocking). - let fast_callers = results - .iter() - .filter(|(_, dur)| *dur < Duration::from_millis(100)) - .count(); - assert!( - fast_callers >= CONCURRENCY - 1, - "expected at least {} fast callers, got {}", - CONCURRENCY - 1, - fast_callers, - ); - - assert_eq!(stats.peak(), 1, "peak concurrency to auth endpoint"); - assert_eq!(stats.total(), 1, "total auth requests"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn test_stress_expired_token_blocks() { - let refresh_delay = Duration::from_millis(200); - let state = DelayedAuthState { - counting: CountingState::new(), - delay: refresh_delay, - }; - let (base_url, stats) = start_axum_server(state).await; - - let refresher = - AccessKeyRefresher::new(SecretToken::new("test-access-key"), base_url, None); - let strategy = Arc::new(AutoRefresh::with_token( - refresher, - make_expired_token("old-token"), - )); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for token in &results { - assert_eq!(token.as_str(), "refreshed-token"); - } - - assert!( - elapsed < refresh_delay + Duration::from_millis(200), - "expected < {:?}, got {:?}", - refresh_delay + Duration::from_millis(200), - elapsed - ); - - assert_eq!(stats.peak(), 1, "peak concurrency to auth endpoint"); - assert_eq!(stats.total(), 1, "total auth requests"); - } -} diff --git a/vendor/stack-auth/src/access_key_strategy.rs b/vendor/stack-auth/src/access_key_strategy.rs deleted file mode 100644 index 3339eee4..00000000 --- a/vendor/stack-auth/src/access_key_strategy.rs +++ /dev/null @@ -1,112 +0,0 @@ -use cts_common::{CtsServiceDiscovery, Region, ServiceDiscovery}; - -use crate::access_key::AccessKey; -use crate::access_key_refresher::AccessKeyRefresher; -use crate::auto_refresh::AutoRefresh; -use crate::{ensure_trailing_slash, AuthError, AuthStrategy, SecretToken, ServiceToken}; - -/// An [`AuthStrategy`] that uses a static access key to authenticate. -/// -/// The first call to [`get_token`](AuthStrategy::get_token) authenticates with -/// the server. Subsequent calls return the cached token until it expires, at -/// which point re-authentication happens automatically. -/// -/// # Example -/// -/// ```no_run -/// use stack_auth::{AccessKey, AccessKeyStrategy}; -/// use cts_common::Region; -/// -/// let region = Region::aws("ap-southeast-2").unwrap(); -/// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap(); -/// let strategy = AccessKeyStrategy::new(region, key).unwrap(); -/// ``` -pub struct AccessKeyStrategy { - inner: AutoRefresh, -} - -impl AccessKeyStrategy { - /// Create a new `AccessKeyStrategy` for the given region and access key. - /// - /// The auth endpoint is resolved automatically via service discovery. - pub fn new(region: Region, access_key: AccessKey) -> Result { - Self::builder(region, access_key).build() - } - - /// Return a builder for configuring an `AccessKeyStrategy` before construction. - /// - /// # Example - /// - /// ```no_run - /// use stack_auth::{AccessKey, AccessKeyStrategy}; - /// use cts_common::Region; - /// - /// let region = Region::aws("ap-southeast-2").unwrap(); - /// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap(); - /// let strategy = AccessKeyStrategy::builder(region, key) - /// .audience("my-audience") - /// .build() - /// .unwrap(); - /// ``` - pub fn builder(region: Region, access_key: AccessKey) -> AccessKeyStrategyBuilder { - AccessKeyStrategyBuilder { - region, - access_key: access_key.into_secret_token(), - audience: None, - base_url_override: None, - } - } -} - -impl AuthStrategy for &AccessKeyStrategy { - async fn get_token(self) -> Result { - Ok(self.inner.get_token().await?) - } -} - -/// Builder for [`AccessKeyStrategy`]. -/// -/// Created via [`AccessKeyStrategy::builder`]. -pub struct AccessKeyStrategyBuilder { - region: Region, - access_key: SecretToken, - audience: Option, - base_url_override: Option, -} - -impl AccessKeyStrategyBuilder { - /// Set the audience for token requests. - pub fn audience(mut self, audience: impl Into) -> Self { - self.audience = Some(audience.into()); - self - } - - /// Override the base URL resolved by service discovery. - /// - /// Useful for pointing at a local or mock auth server during testing. - #[cfg(any(test, feature = "test-utils"))] - pub fn base_url(mut self, url: url::Url) -> Self { - self.base_url_override = Some(url); - self - } - - /// Build the [`AccessKeyStrategy`]. - /// - /// Resolves the base URL via service discovery unless overridden with - /// `base_url` (available when the `test-utils` feature is enabled). - pub fn build(self) -> Result { - let base_url = match self.base_url_override { - Some(url) => url, - None => crate::cts_base_url_from_env()? - .unwrap_or(CtsServiceDiscovery::endpoint(self.region)?), - }; - let refresher = AccessKeyRefresher::new( - self.access_key, - ensure_trailing_slash(base_url), - self.audience, - ); - Ok(AccessKeyStrategy { - inner: AutoRefresh::new(refresher), - }) - } -} diff --git a/vendor/stack-auth/src/auto_refresh.rs b/vendor/stack-auth/src/auto_refresh.rs deleted file mode 100644 index c90dc5e7..00000000 --- a/vendor/stack-auth/src/auto_refresh.rs +++ /dev/null @@ -1,1590 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use tokio::sync::{Mutex, MutexGuard, Notify}; - -use crate::refresher::Refresher; -use crate::{ServiceToken, Token}; - -/// Internal errors from [`AutoRefresh::get_token`]. -/// -/// Strategy wrappers convert these into [`AuthError`](crate::AuthError) for the -/// public API. -#[derive(Debug, thiserror::Error)] -pub(crate) enum AutoRefreshError { - /// No token is cached and the strategy cannot self-authenticate. - #[error("No token found")] - NotFound, - /// The token has expired and refresh failed or is unavailable. - #[error("Token has expired")] - Expired, - /// The refresh/auth HTTP call failed. - #[error("Auth error: {0}")] - Auth(#[from] crate::AuthError), -} - -impl From for crate::AuthError { - fn from(err: AutoRefreshError) -> Self { - match err { - AutoRefreshError::NotFound => crate::AuthError::NotAuthenticated, - AutoRefreshError::Expired => crate::AuthError::TokenExpired, - AutoRefreshError::Auth(e) => e, - } - } -} - -/// Caches a token in memory and uses a [`Refresher`] to re-authenticate -/// or refresh before expiry. -/// -/// See the [crate-level documentation](crate#token-refresh) for a full -/// description of the concurrency model and flow diagram. -pub(crate) struct AutoRefresh { - refresher: R, - state: Mutex, - /// Set to `true` while a refresh HTTP call is in-flight. - /// - /// Stored as an [`AtomicBool`] rather than inside [`State`] so that - /// [`CancelGuard`] can reset it on future cancellation without acquiring - /// the mutex. - refresh_in_progress: AtomicBool, - refresh_notify: Notify, -} - -struct State { - token: Option, -} - -/// Ensures [`AutoRefresh::refresh_in_progress`] is cleared and waiters are -/// notified if the refresh future is cancelled (dropped) before completing. -/// -/// On the normal path (success or handled error), the guard is defused before -/// drop so that the regular cleanup code runs instead. -struct CancelGuard<'a> { - in_progress: &'a AtomicBool, - notify: &'a Notify, - defused: bool, -} - -impl Drop for CancelGuard<'_> { - fn drop(&mut self) { - if !self.defused { - self.in_progress.store(false, Ordering::Release); - self.notify.notify_waiters(); - } - } -} - -impl CancelGuard<'_> { - fn defuse(&mut self) { - self.defused = true; - } -} - -impl State { - fn service_token(&self) -> Result { - let token = self.token.as_ref().ok_or(AutoRefreshError::NotFound)?; - Ok(ServiceToken::new(token.access_token().clone())) - } - - fn require_usable_token(&self) -> Result { - let token = self.token.as_ref().ok_or(AutoRefreshError::NotFound)?; - if token.is_usable() { - Ok(ServiceToken::new(token.access_token().clone())) - } else { - Err(AutoRefreshError::Expired) - } - } -} - -impl AutoRefresh { - /// Create a new `AutoRefresh` with no initial token. - /// - /// The first call to `get_token` will attempt initial authentication via - /// `try_credential(None)` → `refresh()`. Use this for refreshers that can - /// self-authenticate (e.g. access keys). - pub(crate) fn new(refresher: R) -> Self { - Self { - refresher, - state: Mutex::new(State { token: None }), - refresh_in_progress: AtomicBool::new(false), - refresh_notify: Notify::new(), - } - } - - /// Create a new `AutoRefresh` with a pre-loaded token. - /// - /// Use this for refreshers that cannot self-authenticate (e.g. OAuth, - /// which needs a refresh token from a prior device code flow). - pub(crate) fn with_token(refresher: R, token: Token) -> Self { - Self { - refresher, - state: Mutex::new(State { token: Some(token) }), - refresh_in_progress: AtomicBool::new(false), - refresh_notify: Notify::new(), - } - } -} - -impl AutoRefresh { - /// Retrieve a valid access token, refreshing or re-authenticating as needed. - pub(crate) async fn get_token(&self) -> Result { - let mut state = self.state.lock().await; - - if state.token.is_none() { - return self.initial_auth(&mut state).await; - } - - if !state.token.as_ref().is_some_and(|t| t.is_expired()) { - return state.service_token(); - } - - if self.refresh_in_progress.load(Ordering::Acquire) { - return self.wait_for_in_flight_refresh(state).await; - } - - let Some(credential) = self.refresher.try_credential(state.token.as_mut()) else { - return state.require_usable_token(); - }; - - self.refresh_in_progress.store(true, Ordering::Release); - - if state.token.as_ref().is_some_and(|t| t.is_usable()) { - self.refresh_non_blocking(state, credential).await - } else { - self.refresh_blocking(&mut state, credential).await - } - } - - /// No cached token — authenticate via `try_credential(None)`. - /// - /// The lock is held throughout to prevent concurrent initial-auth attempts. - async fn initial_auth(&self, state: &mut State) -> Result { - let Some(credential) = self.refresher.try_credential(None) else { - return Err(AutoRefreshError::NotFound); - }; - self.refresh_in_progress.store(true, Ordering::Release); - let mut guard = CancelGuard { - in_progress: &self.refresh_in_progress, - notify: &self.refresh_notify, - defused: false, - }; - match self.refresher.refresh(&credential).await { - Ok(new_token) => { - self.refresher.save(&new_token); - let service_token = ServiceToken::new(new_token.access_token().clone()); - state.token = Some(new_token); - self.refresh_in_progress.store(false, Ordering::Release); - // Defuse only after the token is installed and the flag cleared, - // so a cancellation anywhere up to here still fires CancelGuard's - // Drop (clears refresh_in_progress + notifies waiters). See CIP-3159. - guard.defuse(); - Ok(service_token) - } - Err(err) => { - guard.defuse(); - self.refresh_in_progress.store(false, Ordering::Release); - Err(AutoRefreshError::Auth(err)) - } - } - } - - /// Another caller is already refreshing — return the current token if still - /// usable, otherwise wait for the in-flight refresh to complete via `Notify`. - /// - /// Takes `MutexGuard` by value because the lock is dropped before awaiting - /// the notification. - async fn wait_for_in_flight_refresh( - &self, - state: MutexGuard<'_, State>, - ) -> Result { - if let Ok(token) = state.service_token() { - if state.token.as_ref().is_some_and(|t| t.is_usable()) { - return Ok(token); - } - } - // Token crossed real expiry during in-flight refresh. Wait for the - // refresh to complete rather than returning Expired. - let notified = self.refresh_notify.notified(); - drop(state); - notified.await; - // Re-check after wake — refresh may have failed. - let state = self.state.lock().await; - state.require_usable_token() - } - - /// Token is expiring but still usable — drop the lock, refresh in the - /// background of this call, and return the old (still-valid) token. - /// - /// Takes `MutexGuard` by value because the lock is dropped before the HTTP - /// request. Notifies waiters after the refresh completes (success or error). - /// - /// A [`CancelGuard`] ensures that if this future is cancelled at any point - /// before the new token is installed — including the post-HTTP, pre-install - /// re-lock window — `refresh_in_progress` is cleared and waiters are - /// notified, so subsequent callers don't hang in - /// [`wait_for_in_flight_refresh`](Self::wait_for_in_flight_refresh). See CIP-3159. - async fn refresh_non_blocking( - &self, - state: MutexGuard<'_, State>, - credential: R::Credential, - ) -> Result { - let current_service_token = state.service_token()?; - drop(state); - - let mut guard = CancelGuard { - in_progress: &self.refresh_in_progress, - notify: &self.refresh_notify, - defused: false, - }; - - match self.refresher.refresh(&credential).await { - Ok(new_token) => { - self.refresher.save(&new_token); - let mut state = self.state.lock().await; - state.token = Some(new_token); - self.refresh_in_progress.store(false, Ordering::Release); - // Defer defuse() past the re-lock + install so a cancellation - // landing on `state.lock().await` still strands neither the flag - // nor waiters. See CIP-3159. - guard.defuse(); - } - Err(err) => { - tracing::warn!(%err, "token refresh failed (token still usable)"); - let mut state = self.state.lock().await; - if let Some(token) = state.token.as_mut() { - self.refresher.restore(token, credential); - } - self.refresh_in_progress.store(false, Ordering::Release); - // Defer defuse() past the re-lock + restore for the same reason - // as the Ok branch (mirror of upstream commit 2ee370561). - guard.defuse(); - } - } - - self.refresh_notify.notify_waiters(); - Ok(current_service_token) - } - - /// Token is fully expired — refresh while holding the lock so concurrent - /// callers block on `lock().await` until the new token is available. - /// - /// A [`CancelGuard`] ensures that if this future is cancelled during the - /// HTTP request, `refresh_in_progress` is cleared and waiters are notified - /// so they don't hang indefinitely. (The credential is lost on cancel — - /// see [`CancelGuard`] docs — but subsequent callers will get `Expired` - /// rather than blocking forever.) - async fn refresh_blocking( - &self, - state: &mut State, - credential: R::Credential, - ) -> Result { - let mut guard = CancelGuard { - in_progress: &self.refresh_in_progress, - notify: &self.refresh_notify, - defused: false, - }; - match self.refresher.refresh(&credential).await { - Ok(new_token) => { - self.refresher.save(&new_token); - let service_token = ServiceToken::new(new_token.access_token().clone()); - state.token = Some(new_token); - self.refresh_in_progress.store(false, Ordering::Release); - // Defuse after install for parity with the other success paths - // (CIP-3159). The lock is held throughout here, so there is no - // await between install and defuse, but keep the invariant uniform. - guard.defuse(); - Ok(service_token) - } - Err(err) => { - guard.defuse(); - tracing::warn!(%err, "token refresh failed"); - if let Some(token) = state.token.as_mut() { - self.refresher.restore(token, credential); - } - self.refresh_in_progress.store(false, Ordering::Release); - Err(AutoRefreshError::Expired) - } - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::oauth_refresher::OAuthRefresher; - use crate::SecretToken; - use mocktail::prelude::*; - use stack_profile::ProfileStore; - use std::sync::Arc; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn make_token(access: &str, expires_in: u64, refresh: bool) -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - Token { - access_token: SecretToken::new(access), - token_type: "Bearer".to_string(), - expires_at: now + expires_in, - refresh_token: if refresh { - Some(SecretToken::new("test-refresh-token")) - } else { - None - }, - region: None, - client_id: None, - device_instance_id: None, - } - } - - fn refresh_response_json(access: &str) -> serde_json::Value { - serde_json::json!({ - "access_token": access, - "token_type": "Bearer", - "expires_in": 3600, - "refresh_token": "new-refresh-token" - }) - } - - fn error_json(error: &str) -> serde_json::Value { - serde_json::json!({ - "error": error, - "error_description": format!("{error} occurred") - }) - } - - async fn start_server(mocks: MockSet) -> MockServer { - let server = MockServer::new_http("auto-refresh-test").with_mocks(mocks); - server.start().await.unwrap(); - server - } - - fn auto_refresh_with_token( - dir: &tempfile::TempDir, - server: &MockServer, - token: Token, - ) -> AutoRefresh { - let store = ProfileStore::new(dir.path()); - store.init_workspace("ZVATKW3VHMFG27DY").unwrap(); - let ws_store = store.current_workspace_store().unwrap(); - ws_store.save_profile(&token).unwrap(); - let refresher = OAuthRefresher::new( - Some(ws_store), - server.url(""), - "cli", - "ap-southeast-2.aws", - None, - ); - AutoRefresh::with_token(refresher, token) - } - - mod given_no_cached_token { - use super::*; - - #[tokio::test] - async fn returns_not_found_for_oauth() { - let server = start_server(MockSet::new()).await; - let store = ProfileStore::new("/tmp/nonexistent"); - let refresher = OAuthRefresher::new( - Some(store), - server.url(""), - "cli", - "ap-southeast-2.aws", - None, - ); - let strategy = AutoRefresh::new(refresher); - - let err = strategy.get_token().await.unwrap_err(); - - assert!( - matches!(err, AutoRefreshError::NotFound), - "expected NotFound, got: {err:?}" - ); - } - } - - mod given_fresh_token { - use super::*; - - #[tokio::test] - async fn returns_cached_token() { - let dir = tempfile::tempdir().unwrap(); - let server = start_server(MockSet::new()).await; - let strategy = - auto_refresh_with_token(&dir, &server, make_token("my-access-token", 3600, false)); - - let token = strategy.get_token().await.unwrap(); - - assert_eq!( - token.as_str(), - "my-access-token", - "should return the cached access token" - ); - } - - #[tokio::test] - async fn caches_across_calls() { - let dir = tempfile::tempdir().unwrap(); - let server = start_server(MockSet::new()).await; - let strategy = - auto_refresh_with_token(&dir, &server, make_token("my-access-token", 3600, false)); - - let token1 = strategy.get_token().await.unwrap(); - assert_eq!( - token1.as_str(), - "my-access-token", - "first call should return the cached token" - ); - - // Delete the file — second call should still return the cached token. - std::fs::remove_file( - dir.path() - .join("workspaces") - .join("ZVATKW3VHMFG27DY") - .join("auth.json"), - ) - .unwrap(); - - let token2 = strategy.get_token().await.unwrap(); - assert_eq!( - token2.as_str(), - "my-access-token", - "second call should return the cached token even after file deletion" - ); - } - - #[tokio::test] - async fn does_not_trigger_refresh() { - // Mock that would fail if hit — proves no refresh request is made. - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.internal_server_error() - .json(error_json("should_not_be_called")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("fresh-token", 3600, true)); - - let token = strategy.get_token().await.unwrap(); - - assert_eq!( - token.as_str(), - "fresh-token", - "should return fresh token without triggering refresh" - ); - } - } - - mod given_fully_expired_token { - use super::*; - - mod without_refresh_token { - use super::*; - - #[tokio::test] - async fn returns_expired() { - let dir = tempfile::tempdir().unwrap(); - let server = start_server(MockSet::new()).await; - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, false)); - - let err = strategy.get_token().await.unwrap_err(); - - assert!( - matches!(err, AutoRefreshError::Expired), - "expected Expired, got: {err:?}" - ); - } - } - - mod with_refresh_token { - use super::*; - - #[tokio::test] - async fn refreshes_and_returns_new_token() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - let token = strategy.get_token().await.unwrap(); - - assert_eq!( - token.as_str(), - "refreshed-token", - "should return the refreshed token" - ); - } - - #[tokio::test] - async fn persists_refreshed_token_to_disk() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - let _ = strategy.get_token().await.unwrap(); - - // Verify the refreshed token was saved to the workspace directory. - let store = ProfileStore::new(dir.path()); - let ws_store = store.current_workspace_store().unwrap(); - let on_disk: Token = ws_store.load_profile().unwrap(); - assert_eq!( - on_disk.access_token().as_str(), - "refreshed-token", - "refreshed token should be persisted to disk" - ); - } - - #[tokio::test] - async fn returns_expired_on_refresh_failure() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("invalid_grant")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - let err = strategy.get_token().await.unwrap_err(); - - assert!( - matches!(err, AutoRefreshError::Expired), - "expected Expired after failed refresh, got: {err:?}" - ); - } - - #[tokio::test] - async fn restores_refresh_token_after_failure() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("invalid_grant")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - // First call: refresh fails, returns Expired. - let err = strategy.get_token().await.unwrap_err(); - assert!( - matches!(err, AutoRefreshError::Expired), - "expected Expired on first attempt, got: {err:?}" - ); - - // Verify the refresh token was restored so a retry is possible. - let state = strategy.state.lock().await; - assert!( - state.token.is_some(), - "token should still be cached after failed refresh" - ); - assert!( - state.token.as_ref().unwrap().refresh_token().is_some(), - "refresh token should be restored for retry" - ); - drop(state); - - // Replace mock with a success response. - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - - // Second call: refresh token is available → retry succeeds. - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "refreshed-token", - "retry should succeed with restored refresh token" - ); - } - - #[tokio::test] - async fn sequential_calls_only_refresh_once() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-once")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - // First call triggers refresh. - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "refreshed-once", - "first call should trigger refresh" - ); - - // Swap mock to track if another refresh is attempted. - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-twice")); - }); - - // Calls 2-5: the refreshed token is fresh, so no further refresh. - for _ in 0..4 { - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "refreshed-once", - "should return cached refreshed token, not trigger another refresh" - ); - } - } - - #[tokio::test] - async fn prevents_second_refresh_after_success() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = - auto_refresh_with_token(&dir, &server, make_token("old-token", 0, true)); - - // First call refreshes successfully. - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "refreshed-token", - "first call should refresh the token" - ); - - // Replace the mock with one that errors. - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("should_not_be_called")); - }); - - // Second call should return the refreshed token without hitting - // the server again (the new token has a fresh expiry). - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "refreshed-token", - "second call should return cached refreshed token" - ); - } - } - } - - mod given_expiring_but_usable_token { - use super::*; - - mod when_refresh_fails { - use super::*; - - #[tokio::test] - async fn returns_current_token() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("server_error")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - // Token expires in 30s (within the 90s leeway so is_expired() = true), - // but the access token is still technically usable. - let strategy = - auto_refresh_with_token(&dir, &server, make_token("still-usable", 30, true)); - - // The refresh fails, but the access token should still be returned - // because it's still usable (30s remaining > 0). - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "still-usable", - "should return still-usable token despite failed refresh" - ); - - // Verify the access token and refresh token are still present. - let state = strategy.state.lock().await; - assert!(state.token.is_some(), "token should still be cached"); - assert_eq!( - state.token.as_ref().unwrap().access_token().as_str(), - "still-usable", - "access token should be unchanged after failed refresh" - ); - assert!( - state.token.as_ref().unwrap().refresh_token().is_some(), - "refresh token should be restored after failed refresh" - ); - } - - #[tokio::test] - async fn restores_refresh_token_for_retry() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("server_error")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - // Token expires in 30s — is_expired() = true, is_usable() = true. - let strategy = - auto_refresh_with_token(&dir, &server, make_token("still-usable", 30, true)); - - // First call: refresh fails, but the still-usable token is returned. - let token = strategy.get_token().await.unwrap(); - assert_eq!( - token.as_str(), - "still-usable", - "first call should return still-usable token" - ); - - // Replace mock with a success response. - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - - // Second call: refresh token was restored, so the retry succeeds. - let token = strategy.get_token().await.unwrap(); - assert!( - token.as_str() == "still-usable" || token.as_str() == "refreshed-token", - "expected old or refreshed token, got: {}", - token.as_str() - ); - - // Verify the cache now holds the refreshed token. - let state = strategy.state.lock().await; - assert_eq!( - state.token.as_ref().unwrap().access_token().as_str(), - "refreshed-token", - "cache should hold the refreshed token after retry" - ); - } - } - } - - mod given_concurrent_callers { - use super::*; - - #[tokio::test] - async fn returns_usable_token_while_refreshing() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &server, - make_token("still-usable", 30, true), - )); - - let s1 = Arc::clone(&strategy); - let handle_a = tokio::spawn(async move { s1.get_token().await.unwrap() }); - - let s2 = Arc::clone(&strategy); - let handle_b = tokio::spawn(async move { s2.get_token().await.unwrap() }); - - let (result_a, result_b) = tokio::join!(handle_a, handle_b); - let token_a = result_a.unwrap(); - let token_b = result_b.unwrap(); - - assert!( - token_a.as_str() == "still-usable" || token_a.as_str() == "refreshed-token", - "unexpected token_a: {}", - token_a.as_str() - ); - assert!( - token_b.as_str() == "still-usable" || token_b.as_str() == "refreshed-token", - "unexpected token_b: {}", - token_b.as_str() - ); - } - - #[tokio::test] - async fn blocks_until_refresh_completes() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json("refreshed-token")); - }); - let server = start_server(mocks).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &server, - make_token("expired-token", 0, true), - )); - - let s1 = Arc::clone(&strategy); - let handle_a = tokio::spawn(async move { s1.get_token().await.unwrap() }); - - let s2 = Arc::clone(&strategy); - let handle_b = tokio::spawn(async move { s2.get_token().await.unwrap() }); - - let (result_a, result_b) = tokio::join!(handle_a, handle_b); - let token_a = result_a.unwrap(); - let token_b = result_b.unwrap(); - - assert_eq!( - token_a.as_str(), - "refreshed-token", - "caller a should receive refreshed token" - ); - assert_eq!( - token_b.as_str(), - "refreshed-token", - "caller b should receive refreshed token" - ); - } - } -} - -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod stress_tests { - use super::*; - use crate::oauth_refresher::OAuthRefresher; - use crate::SecretToken; - use stack_profile::ProfileStore; - use std::sync::atomic::{AtomicUsize, Ordering}; - use std::sync::Arc; - use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - - /// Tracks in-flight and peak concurrency for test assertions. - #[derive(Clone)] - struct CountingState { - total: Arc, - current: Arc, - peak: Arc, - } - - impl CountingState { - fn new() -> Self { - Self { - total: Arc::new(AtomicUsize::new(0)), - current: Arc::new(AtomicUsize::new(0)), - peak: Arc::new(AtomicUsize::new(0)), - } - } - - fn enter(&self) { - self.total.fetch_add(1, Ordering::SeqCst); - let prev = self.current.fetch_add(1, Ordering::SeqCst); - self.peak.fetch_max(prev + 1, Ordering::SeqCst); - } - - fn exit(&self) { - self.current.fetch_sub(1, Ordering::SeqCst); - } - - fn peak(&self) -> usize { - self.peak.load(Ordering::SeqCst) - } - - fn total(&self) -> usize { - self.total.load(Ordering::SeqCst) - } - } - - #[derive(Clone)] - struct DelayedRefreshState { - counting: CountingState, - delay: Duration, - } - - async fn delayed_refresh_handler( - axum::extract::State(state): axum::extract::State, - ) -> axum::Json { - state.counting.enter(); - tokio::time::sleep(state.delay).await; - state.counting.exit(); - axum::Json(serde_json::json!({ - "access_token": "refreshed-token", - "token_type": "Bearer", - "expires_in": 3600, - "refresh_token": "new-refresh-token" - })) - } - - async fn delayed_error_handler( - axum::extract::State(state): axum::extract::State, - ) -> (axum::http::StatusCode, axum::Json) { - state.counting.enter(); - tokio::time::sleep(state.delay).await; - state.counting.exit(); - ( - axum::http::StatusCode::BAD_REQUEST, - axum::Json(serde_json::json!({ - "error": "invalid_grant", - "error_description": "invalid_grant occurred" - })), - ) - } - - async fn start_axum_server( - handler: H, - state: DelayedRefreshState, - ) -> (url::Url, CountingState) - where - H: axum::handler::Handler + Clone + Send + 'static, - T: 'static, - { - let counting = state.counting.clone(); - let app = axum::Router::new() - .route("/oauth/token", axum::routing::post(handler)) - .with_state(state); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - let base_url = url::Url::parse(&format!("http://{addr}")).unwrap(); - (base_url, counting) - } - - fn make_token(access: &str, expires_in: u64, refresh: bool) -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - Token { - access_token: SecretToken::new(access), - token_type: "Bearer".to_string(), - expires_at: now + expires_in, - refresh_token: if refresh { - Some(SecretToken::new("test-refresh-token")) - } else { - None - }, - region: None, - client_id: None, - device_instance_id: None, - } - } - - fn auto_refresh_with_token( - dir: &tempfile::TempDir, - base_url: &url::Url, - token: Token, - ) -> AutoRefresh { - let store = ProfileStore::new(dir.path()); - store.init_workspace("ZVATKW3VHMFG27DY").unwrap(); - let ws_store = store.current_workspace_store().unwrap(); - ws_store.save_profile(&token).unwrap(); - let refresher = OAuthRefresher::new( - Some(ws_store), - base_url.clone(), - "cli", - "ap-southeast-2.aws", - None, - ); - AutoRefresh::with_token(refresher, token) - } - - const CONCURRENCY: usize = 50; - - mod given_fresh_token { - use super::*; - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn all_callers_return_immediately() { - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: Duration::from_millis(500), - }; - let (base_url, stats) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("fresh-token", 3600, true), - )); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for token in &results { - assert_eq!( - token.as_str(), - "fresh-token", - "all callers should receive the fresh token" - ); - } - - assert!( - elapsed < Duration::from_millis(200), - "expected < 200ms for fresh tokens, got {:?}", - elapsed - ); - assert_eq!(stats.total(), 0, "no refresh requests should be made"); - } - } - - mod given_expiring_but_usable_token { - use super::*; - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn non_blocking_reads_during_refresh() { - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: Duration::from_millis(500), - }; - let (base_url, stats) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("still-usable", 30, true), - )); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { - let call_start = Instant::now(); - let token = s.get_token().await.unwrap(); - (token, call_start.elapsed()) - })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for (token, _) in &results { - assert!( - token.as_str() == "still-usable" || token.as_str() == "refreshed-token", - "unexpected token: {}", - token.as_str() - ); - } - - let fast_callers = results - .iter() - .filter(|(_, dur)| *dur < Duration::from_millis(100)) - .count(); - assert!( - fast_callers >= CONCURRENCY - 1, - "expected at least {} fast callers, got {} (total elapsed: {:?})", - CONCURRENCY - 1, - fast_callers, - elapsed - ); - - assert_eq!(stats.peak(), 1, "peak concurrency to refresh endpoint"); - assert_eq!(stats.total(), 1, "total refresh requests"); - } - - /// Reproduces the race condition where a token crosses real expiry during - /// an in-flight non-blocking refresh. Before the fix, late-arriving callers - /// would see `refresh_in_progress = true` + `!is_usable()` and return - /// `Err(Expired)` instead of waiting for the refresh to complete. - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn waiters_receive_token_when_expiry_crosses() { - // Token with 1s until real expiry (minimum granularity since - // expires_at is in seconds). is_expired() = true (within 90s leeway), - // is_usable() = true (1s remaining). Refresh takes 1.5s so the token - // crosses real expiry mid-refresh. - let refresh_delay = Duration::from_millis(1500); - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: refresh_delay, - }; - let (base_url, stats) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("expiring-soon", 1, true), - )); - - // First caller triggers the non-blocking refresh and gets the old token. - let first = strategy.get_token().await.unwrap(); - assert_eq!( - first.as_str(), - "expiring-soon", - "first caller should receive the expiring token" - ); - - // Wait for the token to cross real expiry (but refresh is still in-flight). - tokio::time::sleep(Duration::from_millis(1100)).await; - - // Launch 50 concurrent callers. Without the fix, these would all get - // Err(Expired) because refresh_in_progress = true and !is_usable(). - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - - // All callers must succeed — none should get Expired. - for (i, result) in results.iter().enumerate() { - assert!( - result.is_ok(), - "caller {i} got Err({:?}), expected Ok", - result.as_ref().unwrap_err() - ); - assert_eq!( - result.as_ref().unwrap().as_str(), - "refreshed-token", - "caller {i} should receive the refreshed token" - ); - } - - assert_eq!(stats.total(), 1, "only one refresh request should be made"); - } - } - - mod given_fully_expired_token { - use super::*; - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn all_callers_block_until_refresh() { - let refresh_delay = Duration::from_millis(200); - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: refresh_delay, - }; - let (base_url, stats) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("expired-token", 0, true), - )); - - let start = Instant::now(); - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - let elapsed = start.elapsed(); - - for token in &results { - assert_eq!( - token.as_str(), - "refreshed-token", - "all callers should receive refreshed token" - ); - } - - assert!( - elapsed < refresh_delay + Duration::from_millis(200), - "expected < {:?} for blocked callers, got {:?}", - refresh_delay + Duration::from_millis(200), - elapsed - ); - - assert_eq!(stats.peak(), 1, "peak concurrency to refresh endpoint"); - assert_eq!(stats.total(), 1, "total refresh requests"); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn all_callers_receive_expired_on_failure() { - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: Duration::from_millis(10), - }; - let (base_url, stats) = start_axum_server(delayed_error_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("expired-token", 0, true), - )); - - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - - for result in &results { - assert!(result.is_err(), "expected Expired error, got Ok"); - let err = result.as_ref().unwrap_err(); - assert!( - matches!(err, AutoRefreshError::Expired), - "expected Expired, got: {err:?}" - ); - } - - let state = strategy.state.lock().await; - assert!( - state.token.as_ref().unwrap().refresh_token().is_some(), - "refresh token should be restored after failed refresh" - ); - drop(state); - - assert_eq!(stats.peak(), 1, "peak concurrency to refresh endpoint"); - assert!( - stats.total() >= 1, - "at least one refresh attempt should be made" - ); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn retry_succeeds_after_failure() { - // Phase 1: Server returns errors. - let counting1 = CountingState::new(); - let state1 = DelayedRefreshState { - counting: counting1.clone(), - delay: Duration::from_millis(50), - }; - let (base_url, _) = start_axum_server(delayed_error_handler, state1).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("expired-token", 0, true), - )); - - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy); - handles.push(tokio::spawn(async move { s.get_token().await })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - - for result in &results { - assert!( - result.is_err(), - "first wave: expected Expired, got Ok({})", - result.as_ref().unwrap().as_str() - ); - } - - // Phase 2: New server that returns success. - let counting2 = CountingState::new(); - let state2 = DelayedRefreshState { - counting: counting2.clone(), - delay: Duration::from_millis(50), - }; - let (base_url2, stats2) = start_axum_server(delayed_refresh_handler, state2).await; - - let strategy2 = Arc::new(auto_refresh_with_token( - &dir, - &base_url2, - make_token("expired-token", 0, true), - )); - - let mut handles = Vec::with_capacity(CONCURRENCY); - for _ in 0..CONCURRENCY { - let s = Arc::clone(&strategy2); - handles.push(tokio::spawn(async move { s.get_token().await.unwrap() })); - } - - let results: Vec<_> = { - let mut results = Vec::with_capacity(handles.len()); - for handle in handles { - results.push(handle.await.unwrap()); - } - results - }; - - for token in &results { - assert_eq!( - token.as_str(), - "refreshed-token", - "retry callers should receive refreshed token" - ); - } - - assert_eq!(stats2.total(), 1, "only one retry refresh should be made"); - } - } - - mod given_cancelled_refresh { - use super::*; - - /// If a blocking refresh (fully expired token) is cancelled mid-flight, - /// the `CancelGuard` must reset `refresh_in_progress` and notify waiters - /// so the next caller doesn't hang in `wait_for_in_flight_refresh`. - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn blocked_callers_recover_after_cancellation() { - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: Duration::from_secs(10), // Very slow — will be cancelled - }; - let (base_url, _) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("expired-token", 0, true), - )); - - // Spawn get_token and let the blocking refresh start. - let s = Arc::clone(&strategy); - let handle = tokio::spawn(async move { s.get_token().await }); - tokio::time::sleep(Duration::from_millis(100)).await; - - // Cancel the refresh mid-flight. - handle.abort(); - let _ = handle.await; - - // The next caller must not hang. The credential is lost (refresh - // token was taken before the HTTP call), so the result is Expired, - // but the important thing is that it completes promptly. - let s = Arc::clone(&strategy); - let result = tokio::time::timeout(Duration::from_secs(2), s.get_token()).await; - - assert!( - result.is_ok(), - "get_token() should not hang after cancelled blocking refresh" - ); - } - - /// If a non-blocking refresh (expiring-but-usable token) is cancelled - /// mid-flight, the `CancelGuard` must reset `refresh_in_progress` and - /// notify waiters so they don't hang once the token crosses real expiry. - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn non_blocking_callers_recover_after_cancellation() { - let counting = CountingState::new(); - let state = DelayedRefreshState { - counting: counting.clone(), - delay: Duration::from_secs(10), // Very slow — will be cancelled - }; - let (base_url, _) = start_axum_server(delayed_refresh_handler, state).await; - let dir = tempfile::tempdir().unwrap(); - // Token expires in 30s — is_expired() = true, is_usable() = true. - let strategy = Arc::new(auto_refresh_with_token( - &dir, - &base_url, - make_token("still-usable", 30, true), - )); - - // Spawn get_token — triggers non-blocking refresh, drops lock, then - // blocks on the slow HTTP call. - let s = Arc::clone(&strategy); - let handle = tokio::spawn(async move { s.get_token().await }); - tokio::time::sleep(Duration::from_millis(100)).await; - - // Cancel the refresh mid-flight. - handle.abort(); - let _ = handle.await; - - // The next caller must not hang. The token is still usable so it - // should be returned even though the refresh was cancelled. - let s = Arc::clone(&strategy); - let result = tokio::time::timeout(Duration::from_secs(2), s.get_token()).await; - - assert!( - result.is_ok(), - "get_token() should not hang after cancelled non-blocking refresh" - ); - let result = result.unwrap(); - assert!( - result.is_ok(), - "expected Ok with still-usable token, got: {:?}", - result.unwrap_err() - ); - } - } -} - -/// Regression test for CIP-3159 (backported into this vendored crate by Proxy). -/// -/// A `get_token()` future cancelled in the post-HTTP, pre-install window of -/// [`AutoRefresh::refresh_non_blocking`] must NOT strand -/// `refresh_in_progress = true`. The pre-fix code called `guard.defuse()` -/// before re-acquiring the state lock, so a cancellation landing on that -/// `state.lock().await` left the flag set with no `notify_waiters()` — wedging -/// every later refresh. Once the cached token crossed its real expiry, callers -/// then hung forever in [`AutoRefresh::wait_for_in_flight_refresh`], surfacing -/// in Proxy as `ZeroKMS error: Request not authorized` ~15 min (the access-token -/// lifetime) after startup. -#[cfg(test)] -#[allow(clippy::unwrap_used)] -mod regression_cip_3159 { - use super::*; - use crate::access_key_refresher::AccessKeyRefresher; - use crate::SecretToken; - use std::sync::atomic::Ordering; - use std::sync::Arc; - use std::time::{Duration, SystemTime, UNIX_EPOCH}; - - /// `/api/authorise` handler that sleeps `delay` before returning a valid - /// access-key token response, giving the test a window to cancel in. - async fn delayed_authorise_handler( - axum::extract::State(delay): axum::extract::State, - ) -> axum::Json { - tokio::time::sleep(delay).await; - axum::Json(serde_json::json!({ - "accessToken": "refreshed-token", - "expiry": 3600 - })) - } - - async fn start_authorise_server(delay: Duration) -> url::Url { - let app = axum::Router::new() - .route( - "/api/authorise", - axum::routing::post(delayed_authorise_handler), - ) - .with_state(delay); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - url::Url::parse(&format!("http://{addr}")).unwrap() - } - - /// is_expired() == true (within the 90s leeway, so `get_token` refreshes), - /// but is_usable() == true for `secs_until_expiry` (so it takes the - /// non-blocking path). - fn expiring_but_usable_token(access: &str, secs_until_expiry: u64) -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - Token { - access_token: SecretToken::new(access), - token_type: "Bearer".to_string(), - expires_at: now + secs_until_expiry, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] - async fn cancellation_in_relock_window_does_not_strand_refresh() { - let http_delay = Duration::from_millis(400); - let base_url = start_authorise_server(http_delay).await; - - let strategy = Arc::new(AutoRefresh::with_token( - AccessKeyRefresher::new( - SecretToken::new("CSAKtestKeyId.testKeySecret"), - base_url, - None, - ), - expiring_but_usable_token("old-usable", 2), - )); - - // Caller A drives the refresh: it locks state, sets the in-progress - // flag, drops the lock, then awaits the (slow) HTTP authorise call. - let a = Arc::clone(&strategy); - let handle = tokio::spawn(async move { a.get_token().await }); - - // Let A reach the HTTP await, then take the state lock so that when A's - // request completes it parks on its post-HTTP `state.lock().await` - // instead of installing the new token. - tokio::time::sleep(Duration::from_millis(100)).await; - let held = strategy.state.lock().await; - - // A's HTTP completes (~400ms) and blocks on the lock we hold. - tokio::time::sleep(http_delay + Duration::from_millis(200)).await; - assert!( - strategy.refresh_in_progress.load(Ordering::Acquire), - "precondition: a refresh should be in flight while caller A is parked", - ); - - // Cancel A precisely in the post-HTTP, pre-install window. - handle.abort(); - let _ = handle.await; - drop(held); - - // The CancelGuard's Drop must have cleared the flag on cancellation. - // Pre-fix, defuse() ran before the re-lock, so this stays `true`. - assert!( - !strategy.refresh_in_progress.load(Ordering::Acquire), - "refresh_in_progress stranded `true` after cancellation in the re-lock window (CIP-3159)", - ); - - // End-to-end: once the cached token crosses real expiry, a stranded flag - // would route the next caller into wait_for_in_flight_refresh and hang on - // a notify that never comes. With the fix, the caller re-authenticates. - tokio::time::sleep(Duration::from_millis(2100)).await; - let b = Arc::clone(&strategy); - let result = - tokio::time::timeout(Duration::from_secs(3), async move { b.get_token().await }).await; - assert!( - matches!(result, Ok(Ok(_))), - "get_token() hung or failed after cancellation — refresh wedged (CIP-3159): {result:?}", - ); - } -} diff --git a/vendor/stack-auth/src/auto_strategy.rs b/vendor/stack-auth/src/auto_strategy.rs deleted file mode 100644 index 55f92ef9..00000000 --- a/vendor/stack-auth/src/auto_strategy.rs +++ /dev/null @@ -1,389 +0,0 @@ -use cts_common::Crn; - -use crate::access_key_strategy::AccessKeyStrategy; -use crate::oauth_strategy::OAuthStrategy; -use stack_profile::ProfileStore; - -use crate::{AuthError, AuthStrategy, ServiceToken, Token}; - -/// An [`AuthStrategy`] that automatically detects available credentials -/// and delegates to the appropriate inner strategy. -/// -/// # Detection order -/// -/// 1. If the `CS_CLIENT_ACCESS_KEY` environment variable is set, an -/// [`AccessKeyStrategy`] is created. The region is extracted from the -/// `CS_WORKSPACE_CRN` environment variable. -/// 2. If a token store file exists at the default location -/// (`~/.cipherstash/auth.json`), an [`OAuthStrategy`] is created from it. -/// 3. Otherwise, [`AuthError::NotAuthenticated`] is returned. -/// -/// # Examples -/// -/// ```no_run -/// use stack_auth::{AuthStrategy, AutoStrategy}; -/// -/// # async fn run() -> Result<(), Box> { -/// // Auto-detect from env vars + profile store -/// let strategy = AutoStrategy::detect()?; -/// let token = (&strategy).get_token().await?; -/// println!("Authenticated! token={:?}", token); -/// # Ok(()) -/// # } -/// ``` -/// -/// ```no_run -/// use stack_auth::AutoStrategy; -/// -/// # fn run() -> Result<(), Box> { -/// // Provide explicit values with env/profile fallback -/// let strategy = AutoStrategy::builder() -/// .with_access_key("CSAK...") -/// .detect()?; -/// # Ok(()) -/// # } -/// ``` -pub enum AutoStrategy { - /// Authenticated via a static access key. - AccessKey(AccessKeyStrategy), - /// Authenticated via OAuth tokens persisted on disk. - OAuth(OAuthStrategy), -} - -impl AutoStrategy { - /// Create a builder for configuring credential resolution. - /// - /// The builder lets callers provide explicit values (access key, workspace CRN) - /// that take precedence over environment variables and the profile store. - /// - /// # Example - /// - /// ```no_run - /// use stack_auth::AutoStrategy; - /// use cts_common::Crn; - /// - /// # fn run() -> Result<(), Box> { - /// let crn: Crn = "crn:ap-southeast-2.aws:workspace-id".parse()?; - /// let strategy = AutoStrategy::builder() - /// .with_access_key("CSAKmyKeyId.myKeySecret") - /// .with_workspace_crn(crn) - /// .detect()?; - /// # Ok(()) - /// # } - /// ``` - pub fn builder() -> AutoStrategyBuilder { - AutoStrategyBuilder { - access_key: None, - crn: None, - } - } - - /// Detect credentials from environment variables and profile store. - /// - /// Equivalent to `AutoStrategy::builder().detect()`. - /// - /// Resolution order: - /// 1. `CS_CLIENT_ACCESS_KEY` env var → [`AccessKeyStrategy`] - /// 2. `~/.cipherstash/auth.json` → [`OAuthStrategy`] - /// 3. [`AuthError::NotAuthenticated`] - pub fn detect() -> Result { - Self::builder().detect() - } - - /// Core detection logic, separated for testability. - /// - /// Takes pre-resolved inputs rather than reading from the environment - /// or filesystem directly. - fn detect_inner( - access_key: Option, - crn: Option, - store: Option, - ) -> Result { - // 1. Access key from environment - if let Some(access_key) = access_key { - let region = crn - .map(|c| c.region) - .ok_or(AuthError::MissingWorkspaceCrn)?; - let key: crate::AccessKey = access_key.parse()?; - let strategy = AccessKeyStrategy::new(region, key)?; - return Ok(Self::AccessKey(strategy)); - } - - // 2. OAuth token from disk (in the current workspace directory) - if let Some(store) = store { - let has_token = store - .current_workspace_store() - .map(|ws| ws.exists_profile::()) - .unwrap_or(false); - if has_token { - let strategy = OAuthStrategy::with_profile(store).build()?; - return Ok(Self::OAuth(strategy)); - } - } - - // 3. No credentials found - Err(AuthError::NotAuthenticated) - } -} - -/// Builder for configuring credential resolution before calling [`detect()`](AutoStrategyBuilder::detect). -/// -/// Explicit values provided via builder methods take precedence over environment variables. -/// Environment variables take precedence over the profile store. -/// -/// # Example -/// -/// ```no_run -/// use stack_auth::AutoStrategy; -/// -/// # fn run() -> Result<(), Box> { -/// // Provide access key explicitly, region from CS_WORKSPACE_CRN env var -/// let strategy = AutoStrategy::builder() -/// .with_access_key("CSAKmyKeyId.myKeySecret") -/// .detect()?; -/// # Ok(()) -/// # } -/// ``` -pub struct AutoStrategyBuilder { - access_key: Option, - crn: Option, -} - -impl AutoStrategyBuilder { - /// Provide an explicit access key. Takes precedence over env vars. - pub fn with_access_key(mut self, access_key: impl Into) -> Self { - self.access_key = Some(access_key.into()); - self - } - - /// Provide an explicit workspace CRN. Takes precedence over env vars. - pub fn with_workspace_crn(mut self, crn: Crn) -> Self { - self.crn = Some(crn); - self - } - - /// Resolve the auth strategy. - /// - /// Resolution order: - /// 1. Explicit values provided via builder methods - /// 2. Environment variables (`CS_CLIENT_ACCESS_KEY`, `CS_WORKSPACE_CRN`) - /// 3. Profile store (`~/.cipherstash/auth.json` for OAuth) - /// 4. [`AuthError::NotAuthenticated`] - pub fn detect(self) -> Result { - // Merge explicit values with env vars (explicit wins) - let access_key = self - .access_key - .or_else(|| std::env::var("CS_CLIENT_ACCESS_KEY").ok()); - - let crn = match self.crn { - Some(crn) => Some(crn), - None => std::env::var("CS_WORKSPACE_CRN") - .ok() - .map(|s| s.parse::().map_err(AuthError::InvalidCrn)) - .transpose()?, - }; - - // Resolve errors (e.g. missing profile directory) are intentionally - // swallowed here so that env-var-only setups don't need a profile dir. - // If no credentials are found at all, NotAuthenticated is returned. - let store = ProfileStore::resolve(None).ok(); - - AutoStrategy::detect_inner(access_key, crn, store) - } -} - -impl AuthStrategy for &AutoStrategy { - async fn get_token(self) -> Result { - match self { - AutoStrategy::AccessKey(inner) => inner.get_token().await, - AutoStrategy::OAuth(inner) => inner.get_token().await, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{SecretToken, Token}; - use std::time::{SystemTime, UNIX_EPOCH}; - - const VALID_CRN: &str = "crn:ap-southeast-2.aws:ZVATKW3VHMFG27DY"; - - fn valid_crn() -> Crn { - VALID_CRN.parse().unwrap() - } - - fn make_oauth_token() -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let claims = serde_json::json!({ - "iss": "https://cts.example.com/", - "sub": "CS|test-user", - "aud": "test-audience", - "iat": now, - "exp": now + 3600, - "workspace": "ZVATKW3VHMFG27DY", - "scope": "", - }); - - let key = jsonwebtoken::EncodingKey::from_secret(b"test-secret"); - let jwt = jsonwebtoken::encode(&jsonwebtoken::Header::default(), &claims, &key).unwrap(); - - Token { - access_token: SecretToken::new(jwt), - token_type: "Bearer".to_string(), - expires_at: now + 3600, - refresh_token: Some(SecretToken::new("test-refresh-token")), - region: Some("ap-southeast-2.aws".to_string()), - client_id: Some("test-client-id".to_string()), - device_instance_id: None, - } - } - - fn write_token_store(dir: &std::path::Path) -> ProfileStore { - let store = ProfileStore::new(dir); - store.init_workspace("ZVATKW3VHMFG27DY").unwrap(); - let ws_store = store.current_workspace_store().unwrap(); - ws_store.save_profile(&make_oauth_token()).unwrap(); - store - } - - mod detect_inner { - use super::*; - - #[test] - fn access_key_with_valid_crn() { - let result = AutoStrategy::detect_inner( - Some("CSAKtestKeyId.testKeySecret".into()), - Some(valid_crn()), - None, - ); - - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_))); - } - - #[test] - fn access_key_without_crn_returns_missing_workspace_crn() { - let result = - AutoStrategy::detect_inner(Some("CSAKtestKeyId.testKeySecret".into()), None, None); - - assert!(matches!(result, Err(AuthError::MissingWorkspaceCrn))); - } - - #[test] - fn invalid_access_key_format_returns_invalid_access_key() { - let result = - AutoStrategy::detect_inner(Some("not-a-valid-key".into()), Some(valid_crn()), None); - - assert!(matches!(result, Err(AuthError::InvalidAccessKey(_)))); - } - - #[test] - fn oauth_store_with_valid_token() { - let dir = tempfile::tempdir().unwrap(); - let store = write_token_store(dir.path()); - - let result = AutoStrategy::detect_inner(None, None, Some(store)); - - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), AutoStrategy::OAuth(_))); - } - - #[test] - fn oauth_store_without_token_file_returns_not_authenticated() { - let dir = tempfile::tempdir().unwrap(); - let store = ProfileStore::new(dir.path()); - - let result = AutoStrategy::detect_inner(None, None, Some(store)); - - assert!(matches!(result, Err(AuthError::NotAuthenticated))); - } - - #[test] - fn no_credentials_returns_not_authenticated() { - let result = AutoStrategy::detect_inner(None, None, None); - - assert!(matches!(result, Err(AuthError::NotAuthenticated))); - } - - #[test] - fn access_key_takes_priority_over_oauth_store() { - let dir = tempfile::tempdir().unwrap(); - let store = write_token_store(dir.path()); - - let result = AutoStrategy::detect_inner( - Some("CSAKtestKeyId.testKeySecret".into()), - Some(valid_crn()), - Some(store), - ); - - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_))); - } - } - - mod builder { - use super::*; - - #[test] - fn explicit_access_key_and_crn() { - let result = AutoStrategy::builder() - .with_access_key("CSAKtestKeyId.testKeySecret") - .with_workspace_crn(valid_crn()) - .detect(); - - assert!(result.is_ok()); - assert!(matches!(result.unwrap(), AutoStrategy::AccessKey(_))); - } - - #[test] - fn explicit_access_key_without_crn_and_no_env_returns_missing_workspace_crn() { - // Save and clear env to ensure no fallback - let saved_crn = std::env::var("CS_WORKSPACE_CRN").ok(); - std::env::remove_var("CS_WORKSPACE_CRN"); - - let result = AutoStrategy::builder() - .with_access_key("CSAKtestKeyId.testKeySecret") - .detect(); - - // Restore env - if let Some(val) = saved_crn { - std::env::set_var("CS_WORKSPACE_CRN", val); - } - - assert!(matches!(result, Err(AuthError::MissingWorkspaceCrn))); - } - - #[test] - fn invalid_crn_env_var_returns_invalid_crn() { - let saved_crn = std::env::var("CS_WORKSPACE_CRN").ok(); - std::env::set_var("CS_WORKSPACE_CRN", "not-a-crn"); - - let result = AutoStrategy::builder() - .with_access_key("CSAKtestKeyId.testKeySecret") - .detect(); - - // Restore env - match saved_crn { - Some(val) => std::env::set_var("CS_WORKSPACE_CRN", val), - None => std::env::remove_var("CS_WORKSPACE_CRN"), - } - - assert!(matches!(result, Err(AuthError::InvalidCrn(_)))); - } - - #[test] - fn invalid_explicit_access_key_returns_invalid_access_key() { - let result = AutoStrategy::builder() - .with_access_key("not-a-valid-key") - .with_workspace_crn(valid_crn()) - .detect(); - - assert!(matches!(result, Err(AuthError::InvalidAccessKey(_)))); - } - } -} diff --git a/vendor/stack-auth/src/device_client.rs b/vendor/stack-auth/src/device_client.rs deleted file mode 100644 index b32c7047..00000000 --- a/vendor/stack-auth/src/device_client.rs +++ /dev/null @@ -1,318 +0,0 @@ -//! Post-login device client provisioning. -//! -//! After a device-code login, the caller must create a client in ZeroKMS and -//! persist the resulting secret key to disk. This module provides the -//! orchestration logic so that any consumer (not just the CLI) can perform -//! this step. - -use stack_profile::{DeviceIdentity, ProfileStore}; -use uuid::Uuid; -use zerokms_protocol::{CreateClientRequest, CreateClientResponse, ViturKeyMaterial, ViturRequest}; - -use crate::{ensure_trailing_slash, http_client, ServiceToken, Token}; - -fn user_agent() -> String { - format!( - "stack-auth/{} ({} {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH, - ) -} - -// --------------------------------------------------------------------------- -// Secret key file (output) -// --------------------------------------------------------------------------- - -const SECRET_KEY_FILENAME: &str = "secretkey.json"; -const SECRET_KEY_MODE: u32 = 0o600; - -/// The on-disk shape of `secretkey.json`. -/// -/// Must stay in sync with `cipherstash_client::zerokms::SecretKey` which -/// deserializes this file. If that type moves to a shared crate, replace -/// this with a re-export. -#[derive(serde::Serialize)] -struct SecretKeyFile { - client_id: Uuid, - client_key: ViturKeyMaterial, -} - -// --------------------------------------------------------------------------- -// Error type -// --------------------------------------------------------------------------- - -/// Errors that can occur during device client provisioning. -#[derive(Debug, thiserror::Error)] -pub enum DeviceClientError { - /// The profile store could not load or create required data. - #[error("Profile error: {0}")] - Profile(#[from] stack_profile::ProfileError), - - /// Authentication token could not be loaded or decoded. - #[error("Auth error: {0}")] - Auth(#[from] crate::AuthError), - - /// The HTTP request to ZeroKMS failed. - #[error("ZeroKMS request failed: {0}")] - Request(#[from] reqwest::Error), - - /// ZeroKMS returned a non-success, non-conflict status. - #[error("ZeroKMS returned {status}: {body}")] - Server { status: u16, body: String }, - - /// Failed to construct the ZeroKMS endpoint URL. - #[error("Invalid ZeroKMS URL: {0}")] - InvalidUrl(#[from] url::ParseError), -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/// Provision a device client after login. -/// -/// Loads the auth token and device identity from disk, creates a client in -/// ZeroKMS (on the workspace's default keyset), and persists the resulting -/// secret key to the profile store. -/// -/// If the secret key already exists on disk, or the server returns 409 -/// (conflict), this is a no-op. -pub async fn bind_client_device(store: &ProfileStore) -> Result<(), DeviceClientError> { - let ws_store = store.current_workspace_store()?; - - if ws_store.exists(SECRET_KEY_FILENAME) { - tracing::debug!("secret key already exists, skipping provisioning"); - return Ok(()); - } - - let token: Token = ws_store.load_profile()?; - let service_token = ServiceToken::new(token.access_token().clone()); - let zerokms_url = ensure_trailing_slash(service_token.zerokms_url()?); - - // DeviceIdentity is NOT workspace-scoped, so this reads from the root. - let identity = DeviceIdentity::load_or_create(store)?; - - let request = CreateClientRequest { - keyset_id: None, - name: (&identity.device_name).into(), - description: (&identity.device_name).into(), - }; - - let url = zerokms_url.join(CreateClientRequest::ENDPOINT)?; - - let response = http_client() - .post(url) - .header(reqwest::header::USER_AGENT, user_agent()) - .bearer_auth(service_token.as_str()) - .json(&request) - .send() - .await?; - - let status = response.status(); - - if status == reqwest::StatusCode::CONFLICT { - // Another client was already provisioned server-side. - tracing::debug!("device client already exists, skipping"); - return Ok(()); - } - - if !status.is_success() { - let body = response.text().await.unwrap_or_default(); - return Err(DeviceClientError::Server { - status: status.as_u16(), - body, - }); - } - - let created: CreateClientResponse = response.json().await?; - - let secret_key = SecretKeyFile { - client_id: created.id, - client_key: created.client_key, - }; - - ws_store.save_with_mode(SECRET_KEY_FILENAME, &secret_key, SECRET_KEY_MODE)?; - - Ok(()) -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - use crate::SecretToken; - use mocktail::prelude::*; - use tempfile::TempDir; - - fn make_test_jwt(zerokms_url: impl std::fmt::Display) -> String { - use jsonwebtoken::{encode, EncodingKey, Header}; - use std::time::{SystemTime, UNIX_EPOCH}; - - let zerokms_url = zerokms_url.to_string(); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let claims = serde_json::json!({ - "iss": "https://cts.example.com/", - "sub": "CS|test-user", - "aud": "legacy-aud-value", - "iat": now, - "exp": now + 3600, - "workspace": "ZVATKW3VHMFG27DY", - "scope": "", - "services": { - "zerokms": zerokms_url, - }, - }); - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(b"test-secret"), - ) - .unwrap() - } - - const TEST_WORKSPACE_ID: &str = "ZVATKW3VHMFG27DY"; - - fn save_test_token(store: &ProfileStore, access_token: &str) { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let token = Token { - access_token: SecretToken::new(access_token), - refresh_token: None, - token_type: "Bearer".into(), - expires_at: now + 3600, - region: None, - client_id: None, - device_instance_id: None, - }; - store.init_workspace(TEST_WORKSPACE_ID).unwrap(); - let ws_store = store.current_workspace_store().unwrap(); - ws_store.save_profile(&token).unwrap(); - } - - fn client_response_json() -> serde_json::Value { - serde_json::json!({ - "id": "00000000-0000-0000-0000-000000000001", - "dataset_id": "00000000-0000-0000-0000-000000000099", - "name": "test-device", - "description": "test-device", - "client_key": "dGVzdC1rZXktbWF0ZXJpYWw=" - }) - } - - async fn start_server(mocks: MockSet) -> MockServer { - let server = MockServer::new_http("device-client-test").with_mocks(mocks); - server.start().await.unwrap(); - server - } - - #[tokio::test] - async fn provisions_and_saves_secret_key() { - let dir = TempDir::new().unwrap(); - let store = ProfileStore::new(dir.path()); - - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/create-client"); - then.json(client_response_json()); - }); - let server = start_server(mocks).await; - - let jwt = make_test_jwt(server.url("/")); - save_test_token(&store, &jwt); - - bind_client_device(&store).await.unwrap(); - - let ws_store = store.workspace_store(TEST_WORKSPACE_ID).unwrap(); - let saved: serde_json::Value = ws_store.load(SECRET_KEY_FILENAME).unwrap(); - assert_eq!(saved["client_id"], "00000000-0000-0000-0000-000000000001"); - assert_eq!(saved["client_key"], "dGVzdC1rZXktbWF0ZXJpYWw="); - } - - #[tokio::test] - async fn skips_when_secret_key_exists() { - let dir = TempDir::new().unwrap(); - let store = ProfileStore::new(dir.path()); - store.init_workspace(TEST_WORKSPACE_ID).unwrap(); - - // Pre-populate secretkey.json in the workspace directory - let ws_store = store.workspace_store(TEST_WORKSPACE_ID).unwrap(); - ws_store - .save_with_mode( - SECRET_KEY_FILENAME, - &serde_json::json!({"client_id": "old", "client_key": "old"}), - SECRET_KEY_MODE, - ) - .unwrap(); - - // No mock server needed — the HTTP call should never happen. - bind_client_device(&store).await.unwrap(); - - let saved: serde_json::Value = ws_store.load(SECRET_KEY_FILENAME).unwrap(); - assert_eq!( - saved["client_id"], "old", - "should not overwrite existing key" - ); - } - - #[tokio::test] - async fn no_op_on_conflict() { - let dir = TempDir::new().unwrap(); - let store = ProfileStore::new(dir.path()); - - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/create-client"); - then.status(reqwest::StatusCode::CONFLICT) - .json(serde_json::json!({"error": "conflict"})); - }); - let server = start_server(mocks).await; - - let jwt = make_test_jwt(server.url("/")); - save_test_token(&store, &jwt); - - bind_client_device(&store).await.unwrap(); - - let ws_store = store.workspace_store(TEST_WORKSPACE_ID).unwrap(); - assert!( - !ws_store.exists(SECRET_KEY_FILENAME), - "should not write secret key on conflict" - ); - } - - #[tokio::test] - async fn returns_error_on_server_failure() { - let dir = TempDir::new().unwrap(); - let store = ProfileStore::new(dir.path()); - - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/create-client"); - then.status(reqwest::StatusCode::INTERNAL_SERVER_ERROR) - .json(serde_json::json!({"error": "internal error"})); - }); - let server = start_server(mocks).await; - - let jwt = make_test_jwt(server.url("/")); - save_test_token(&store, &jwt); - - let err = bind_client_device(&store).await.unwrap_err(); - assert!( - matches!(err, DeviceClientError::Server { status: 500, .. }), - "expected Server error, got: {err:?}" - ); - } -} diff --git a/vendor/stack-auth/src/device_code/mod.rs b/vendor/stack-auth/src/device_code/mod.rs deleted file mode 100644 index d4daf3a7..00000000 --- a/vendor/stack-auth/src/device_code/mod.rs +++ /dev/null @@ -1,375 +0,0 @@ -mod protocol; - -use cts_common::{CtsServiceDiscovery, Region, ServiceDiscovery}; -use url::Url; - -use std::time::{SystemTime, UNIX_EPOCH}; - -use std::path::PathBuf; - -use stack_profile::ProfileStore; - -use crate::{ensure_trailing_slash, http_client, AuthError, DeviceIdentity, Token}; -use protocol::{ - DeviceCode, DeviceCodeRequest, DeviceCodeResponse, ErrorResponse, TokenRequest, TokenResponse, -}; - -#[cfg(test)] -mod tests; - -/// Authenticates with CipherStash using the -/// [device code flow (RFC 8628)](https://datatracker.ietf.org/doc/html/rfc8628). -/// -/// This is the primary entry point for CLI and browserless authentication. -/// Create a strategy with [`DeviceCodeStrategy::new`], then call -/// [`begin`](DeviceCodeStrategy::begin) to start the flow. -/// -/// # Example -/// -/// ``` -/// use stack_auth::DeviceCodeStrategy; -/// use cts_common::Region; -/// -/// let region = Region::aws("ap-southeast-2").unwrap(); -/// let strategy = DeviceCodeStrategy::new(region, "my-client-id").unwrap(); -/// ``` -pub struct DeviceCodeStrategy { - region: Region, - base_url: Url, - client_id: String, - profile_dir: Option, - device_identity: Option, -} - -impl DeviceCodeStrategy { - /// Create a new strategy for the given CipherStash region and OAuth client ID. - /// - /// The auth endpoint is resolved automatically via service discovery. - /// - /// # Example - /// - /// ``` - /// use stack_auth::DeviceCodeStrategy; - /// use cts_common::Region; - /// - /// let strategy = DeviceCodeStrategy::new( - /// Region::aws("ap-southeast-2").unwrap(), - /// "my-client-id", - /// ).unwrap(); - /// ``` - pub fn new(region: Region, client_id: impl Into) -> Result { - Self::builder(region, client_id).build() - } - - /// Return a builder for configuring a `DeviceCodeStrategy` before construction. - pub fn builder(region: Region, client_id: impl Into) -> DeviceCodeStrategyBuilder { - DeviceCodeStrategyBuilder { - region, - client_id: client_id.into(), - base_url_override: None, - profile_dir: None, - device_identity: None, - } - } - - /// Start the device code flow. - /// - /// Requests a device code from the CipherStash auth server and returns a - /// [`PendingDeviceCode`] with the user-facing codes and URIs. Show these - /// to the user, then call [`PendingDeviceCode::poll_for_token`] to wait - /// for authorization. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidClient`] if the client ID is not recognized, - /// or [`AuthError::Request`] if the server is unreachable. - pub async fn begin(&self) -> Result { - let client = http_client(); - - let code_url = self.base_url.join("oauth/device/code")?; - - tracing::debug!(url = %code_url, client_id = %self.client_id, "requesting device code"); - - let device_instance_id = self - .device_identity - .as_ref() - .map(|d| d.device_instance_id.to_string()); - - let code_resp = client - .post(code_url) - .form(&DeviceCodeRequest { - client_id: &self.client_id, - device_instance_id: device_instance_id.as_deref(), - device_name: self - .device_identity - .as_ref() - .map(|d| d.device_name.as_str()), - }) - .send() - .await?; - - if !code_resp.status().is_success() { - let err: ErrorResponse = code_resp.json().await?; - tracing::debug!(error = %err.error, "device code request failed"); - return Err(match err.error.as_str() { - "invalid_client" => AuthError::InvalidClient, - _ => AuthError::Server(err.error_description), - }); - } - - let code: DeviceCodeResponse = code_resp.json().await?; - - let token_url = self.base_url.join("oauth/device/token")?; - - tracing::debug!( - user_code = %code.user_code, - expires_in = code.expires_in, - "device code received" - ); - - Ok(PendingDeviceCode { - token_url, - region: self.region, - client_id: self.client_id.clone(), - device_code: code.device_code, - user_code: code.user_code, - verification_uri: code.verification_uri, - verification_uri_complete: code.verification_uri_complete, - expires_in: code.expires_in, - profile_dir: self.profile_dir.clone(), - device_identity: self.device_identity.clone(), - }) - } -} - -/// Builder for [`DeviceCodeStrategy`]. -/// -/// Created via [`DeviceCodeStrategy::builder`]. -pub struct DeviceCodeStrategyBuilder { - region: Region, - client_id: String, - base_url_override: Option, - profile_dir: Option, - device_identity: Option, -} - -impl DeviceCodeStrategyBuilder { - /// Override the base URL resolved by service discovery. - /// - /// Useful for pointing at a local or mock CTS instance during testing. - #[cfg(any(test, feature = "test-utils"))] - pub fn base_url(mut self, url: Url) -> Self { - self.base_url_override = Some(url); - self - } - - /// Override the profile directory used to persist the token. - /// - /// By default tokens are saved to `~/.cipherstash/auth.json`. Use this in - /// tests to redirect writes to a temporary directory. - #[cfg(any(test, feature = "test-utils"))] - pub fn profile_dir(mut self, dir: impl Into) -> Self { - self.profile_dir = Some(dir.into()); - self - } - - /// Set the device identity for this strategy. - /// - /// When set, the device instance ID and name are sent to the auth server - /// during the device code flow and persisted in the token. - pub fn device_identity(mut self, identity: DeviceIdentity) -> Self { - self.device_identity = Some(identity); - self - } - - /// Build the [`DeviceCodeStrategy`]. - /// - /// Resolves the base URL via service discovery unless overridden with - /// `base_url` (available when the `test-utils` feature is enabled). - pub fn build(self) -> Result { - let base_url = match self.base_url_override { - Some(url) => url, - None => crate::cts_base_url_from_env()? - .unwrap_or(CtsServiceDiscovery::endpoint(self.region)?), - }; - Ok(DeviceCodeStrategy { - region: self.region, - base_url: ensure_trailing_slash(base_url), - client_id: self.client_id, - profile_dir: self.profile_dir, - device_identity: self.device_identity, - }) - } -} - -/// A device code flow that is waiting for the user to authorize. -/// -/// Returned by [`DeviceCodeStrategy::begin`]. Display the -/// [`user_code`](Self::user_code) and -/// [`verification_uri_complete`](Self::verification_uri_complete) to the user -/// (or call [`open_in_browser`](Self::open_in_browser)), then call -/// [`poll_for_token`](Self::poll_for_token) to wait for authorization. -/// -/// # Example -/// -/// ```no_run -/// # use stack_auth::DeviceCodeStrategy; -/// # use cts_common::Region; -/// # async fn run() -> Result<(), Box> { -/// # let strategy = DeviceCodeStrategy::new(Region::aws("ap-southeast-2")?, "cli")?; -/// let pending = strategy.begin().await?; -/// -/// println!("Go to: {}", pending.verification_uri_complete()); -/// println!("Enter code: {}", pending.user_code()); -/// -/// let token = pending.poll_for_token().await?; -/// # Ok(()) -/// # } -/// ``` -#[derive(Debug)] -pub struct PendingDeviceCode { - token_url: Url, - region: Region, - client_id: String, - device_code: DeviceCode, - /// The short code the user must enter to authorize this device. - user_code: String, - /// The base verification URI (without the user code embedded). - verification_uri: String, - /// The full verification URI with the user code pre-filled. - verification_uri_complete: String, - /// How many seconds the device code remains valid. - expires_in: u64, - /// Profile directory override. Falls back to `~/.cipherstash`. - profile_dir: Option, - /// Device identity to associate with the token. - device_identity: Option, -} - -impl PendingDeviceCode { - /// The short code the user must enter to authorize this device. - pub fn user_code(&self) -> &str { - &self.user_code - } - - /// The base verification URI (without the user code embedded). - pub fn verification_uri(&self) -> &str { - &self.verification_uri - } - - /// The full verification URI with the user code pre-filled. - pub fn verification_uri_complete(&self) -> &str { - &self.verification_uri_complete - } - - /// How many seconds the device code remains valid. - pub fn expires_in(&self) -> u64 { - self.expires_in - } - - /// Open the verification URI in the user's default browser. - /// - /// Returns `true` if the browser was opened successfully. - pub fn open_in_browser(&self) -> bool { - open::that(&self.verification_uri_complete).is_ok() - } - - /// Poll the auth server until the user authorizes (or the code expires). - /// - /// This method consumes `self` and blocks asynchronously, polling at a - /// server-controlled interval (starting at 5 seconds). It returns a - /// [`Token`] on success. - /// - /// # Errors - /// - /// - [`AuthError::AccessDenied`] — the user rejected the request. - /// - [`AuthError::TokenExpired`] — the device code expired before the user - /// authorized. - /// - [`AuthError::Request`] — a network error occurred while polling. - pub async fn poll_for_token(self) -> Result { - let client = http_client(); - let mut interval = tokio::time::Duration::from_secs(5); - let deadline = - tokio::time::Instant::now() + tokio::time::Duration::from_secs(self.expires_in); - - tracing::debug!( - url = %self.token_url, - expires_in = self.expires_in, - "polling for token" - ); - - loop { - if tokio::time::Instant::now() >= deadline { - tracing::debug!("device code expired while polling"); - return Err(AuthError::TokenExpired); - } - - let resp = client - .post(self.token_url.clone()) - .form(&TokenRequest { - client_id: &self.client_id, - device_code: &self.device_code, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }) - .send() - .await?; - - if resp.status().is_success() { - tracing::debug!("token received"); - let token_resp: TokenResponse = resp.json().await?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - let mut token = Token { - access_token: token_resp.access_token, - token_type: token_resp.token_type, - expires_at: now + token_resp.expires_in, - refresh_token: token_resp.refresh_token, - region: None, - client_id: None, - device_instance_id: None, - }; - token.set_region(self.region.identifier()); - token.set_client_id(&self.client_id); - if let Some(ref identity) = self.device_identity { - token.set_device_instance_id(identity.device_instance_id.to_string()); - } - - let store = match &self.profile_dir { - Some(dir) => ProfileStore::new(dir), - None => ProfileStore::resolve(None)?, - }; - let workspace_id = token.workspace_id()?; - store.init_workspace(workspace_id.as_str())?; - store - .workspace_store(workspace_id.as_str())? - .save_profile(&token)?; - tracing::debug!( - workspace = workspace_id.as_str(), - "token saved to workspace directory" - ); - - return Ok(token); - } - - let err: ErrorResponse = resp.json().await?; - match err.error.as_str() { - "authorization_pending" => { - tracing::debug!("authorization pending, retrying"); - } - "slow_down" => { - interval += tokio::time::Duration::from_secs(5); - tracing::debug!(interval_secs = interval.as_secs(), "slowing down"); - } - "expired_token" => return Err(AuthError::TokenExpired), - "access_denied" => return Err(AuthError::AccessDenied), - "invalid_grant" => return Err(AuthError::InvalidGrant), - "invalid_client" => return Err(AuthError::InvalidClient), - _ => return Err(AuthError::Server(err.error_description)), - } - - tokio::time::sleep(interval).await; - } - } -} diff --git a/vendor/stack-auth/src/device_code/protocol.rs b/vendor/stack-auth/src/device_code/protocol.rs deleted file mode 100644 index dff03322..00000000 --- a/vendor/stack-auth/src/device_code/protocol.rs +++ /dev/null @@ -1,52 +0,0 @@ -use serde::{Deserialize, Serialize}; -use vitaminc::protected::OpaqueDebug; -use zeroize::ZeroizeOnDrop; - -use crate::SecretToken; - -/// A device code issued by the auth server, exchanged for an access token -/// once the user authorizes. -#[derive(OpaqueDebug, ZeroizeOnDrop, Deserialize, Serialize)] -#[serde(transparent)] -pub(super) struct DeviceCode(String); - -#[derive(Deserialize)] -pub(super) struct DeviceCodeResponse { - pub device_code: DeviceCode, - pub user_code: String, - pub verification_uri: String, - pub verification_uri_complete: String, - pub expires_in: u64, -} - -#[derive(Deserialize)] -pub(super) struct TokenResponse { - pub access_token: SecretToken, - pub token_type: String, - pub expires_in: u64, - #[serde(default)] - pub refresh_token: Option, -} - -#[derive(Deserialize)] -pub(super) struct ErrorResponse { - pub error: String, - #[serde(default)] - pub error_description: String, -} - -#[derive(Serialize)] -pub(super) struct DeviceCodeRequest<'a> { - pub client_id: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - pub device_instance_id: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - pub device_name: Option<&'a str>, -} - -#[derive(Serialize)] -pub(super) struct TokenRequest<'a> { - pub client_id: &'a str, - pub device_code: &'a DeviceCode, - pub grant_type: &'a str, -} diff --git a/vendor/stack-auth/src/device_code/tests.rs b/vendor/stack-auth/src/device_code/tests.rs deleted file mode 100644 index 357c5e31..00000000 --- a/vendor/stack-auth/src/device_code/tests.rs +++ /dev/null @@ -1,423 +0,0 @@ -use super::*; -use cts_common::Region; -use mocktail::prelude::*; -use tempfile::TempDir; - -fn device_code_json() -> serde_json::Value { - serde_json::json!({ - "device_code": "test_device_code", - "user_code": "ABCD-EFGH", - "verification_uri": "http://example.com/activate", - "verification_uri_complete": "http://example.com/activate?user_code=ABCD-EFGH", - "expires_in": 900 - }) -} - -/// Build a valid JWT access token containing a workspace claim. -fn test_access_token() -> String { - use jsonwebtoken::{encode, EncodingKey, Header}; - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let claims = serde_json::json!({ - "iss": "https://cts.example.com/", - "sub": "CS|test-user", - "aud": "test-audience", - "iat": now, - "exp": now + 3600, - "workspace": "ZVATKW3VHMFG27DY", - "scope": "", - }); - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(b"test-secret"), - ) - .unwrap() -} - -fn token_json() -> serde_json::Value { - serde_json::json!({ - "access_token": test_access_token(), - "token_type": "Bearer", - "expires_in": 3600 - }) -} - -fn error_json(error: &str) -> serde_json::Value { - serde_json::json!({ - "error": error, - "error_description": format!("{error} occurred") - }) -} - -fn mock_code_endpoint(mocks: &mut MockSet) { - mocks.mock(|when, then| { - when.post().path("/oauth/device/code"); - then.json(device_code_json()); - }); -} - -async fn start_server(mocks: MockSet) -> MockServer { - let server = MockServer::new_http("stack-auth-test").with_mocks(mocks); - server.start().await.unwrap(); - server -} - -fn strategy_for(server: &MockServer, dir: &TempDir) -> DeviceCodeStrategy { - DeviceCodeStrategy::builder(Region::aws("ap-southeast-2").unwrap(), "cli") - .base_url(server.url("")) - .profile_dir(dir.path()) - .build() - .unwrap() -} - -// ---- begin() tests ---- - -#[tokio::test] -async fn test_begin_returns_pending_device_code() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - let server = start_server(mocks).await; - - let pending = strategy_for(&server, &dir).begin().await.unwrap(); - - assert_eq!(pending.user_code(), "ABCD-EFGH"); - assert_eq!(pending.verification_uri(), "http://example.com/activate"); - assert_eq!( - pending.verification_uri_complete(), - "http://example.com/activate?user_code=ABCD-EFGH" - ); - assert_eq!(pending.expires_in(), 900); -} - -#[tokio::test] -async fn test_begin_invalid_client() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/device/code"); - then.bad_request().json(error_json("invalid_client")); - }); - let server = start_server(mocks).await; - - let err = strategy_for(&server, &dir).begin().await.unwrap_err(); - - assert!(matches!(err, AuthError::InvalidClient)); -} - -#[tokio::test] -async fn test_begin_server_error() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/device/code"); - then.bad_request().json(error_json("server_error")); - }); - let server = start_server(mocks).await; - - let err = strategy_for(&server, &dir).begin().await.unwrap_err(); - - assert!(matches!(&err, AuthError::Server(desc) if desc == "server_error occurred")); -} - -// ---- poll_for_token() tests ---- - -/// Helper: calls begin() against a server that already has the code mock, -/// then returns the PendingDeviceCode ready for polling. -async fn begin_pending(server: &MockServer, dir: &TempDir) -> PendingDeviceCode { - strategy_for(server, dir).begin().await.unwrap() -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_success() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.json(token_json()); - }); - let server = start_server(mocks).await; - - let token = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap(); - - assert_eq!(token.token_type(), "Bearer"); - assert!(!token.is_expired()); - assert!((3598..=3600).contains(&token.expires_in())); - assert_eq!( - token.workspace_id().unwrap().as_str(), - "ZVATKW3VHMFG27DY", - "workspace ID should be extracted from the JWT" - ); - - // Verify the token was persisted to the workspace directory - let store = ProfileStore::new(dir.path()); - assert_eq!( - store.current_workspace().unwrap(), - "ZVATKW3VHMFG27DY", - "current workspace should be set after poll_for_token" - ); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_access_denied() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("access_denied")); - }); - let server = start_server(mocks).await; - - let err = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::AccessDenied)); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_expired_token() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("expired_token")); - }); - let server = start_server(mocks).await; - - let err = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::TokenExpired)); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_invalid_grant() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("invalid_grant")); - }); - let server = start_server(mocks).await; - - let err = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::InvalidGrant)); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_invalid_client() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("invalid_client")); - }); - let server = start_server(mocks).await; - - let err = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::InvalidClient)); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_unknown_error() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("something_unexpected")); - }); - let server = start_server(mocks).await; - - let err = begin_pending(&server, &dir) - .await - .poll_for_token() - .await - .unwrap_err(); - - assert!(matches!(&err, AuthError::Server(desc) if desc == "something_unexpected occurred")); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_authorization_pending_then_success() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("authorization_pending")); - }); - let server = start_server(mocks).await; - let pending = begin_pending(&server, &dir).await; - - // Use tokio::join! so the swap future can borrow server.mocks() directly - // (the shared RwLock) rather than cloning the MockSet. - // First poll at T=5s returns "authorization_pending". - // At T=6s the mock is swapped. Second poll at T=10s returns success. - let (result, _) = tokio::join!(pending.poll_for_token(), async { - tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/device/token"); - then.json(token_json()); - }); - }); - - let token = result.unwrap(); - assert_eq!(token.token_type(), "Bearer"); - assert!( - token.workspace_id().is_ok(), - "token should contain a valid workspace claim" - ); -} - -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_slow_down_then_success() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("slow_down")); - }); - let server = start_server(mocks).await; - let pending = begin_pending(&server, &dir).await; - - // First poll returns "slow_down", interval increases to 10s. - // Swap the mock to return success before the second poll. - let (result, _) = tokio::join!(pending.poll_for_token(), async { - tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; - server.mocks().clear(); - server.mocks().mock(|when, then| { - when.post().path("/oauth/device/token"); - then.json(token_json()); - }); - }); - - let token = result.unwrap(); - assert_eq!(token.token_type(), "Bearer"); - assert!( - token.workspace_id().is_ok(), - "token should contain a valid workspace claim" - ); -} - -/// Proves that `slow_down` increases the poll interval: with a short -/// `expires_in`, the increased interval pushes the next poll past the -/// deadline, causing a `TokenExpired` error. -#[tokio::test(start_paused = true)] -async fn test_poll_for_token_slow_down_increases_interval() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - // expires_in = 12: without slow_down, second poll at T=10 is within - // the deadline. With slow_down, interval becomes 10s, so second poll - // at T=15 exceeds the 12s deadline. - mocks.mock(|when, then| { - when.post().path("/oauth/device/code"); - then.json(serde_json::json!({ - "device_code": "test_device_code", - "user_code": "ABCD-EFGH", - "verification_uri": "http://example.com/activate", - "verification_uri_complete": "http://example.com/activate?user_code=ABCD-EFGH", - "expires_in": 12 - })); - }); - mocks.mock(|when, then| { - when.post().path("/oauth/device/token"); - then.bad_request().json(error_json("slow_down")); - }); - let server = start_server(mocks).await; - let pending = begin_pending(&server, &dir).await; - - let err = pending.poll_for_token().await.unwrap_err(); - - assert!(matches!(err, AuthError::TokenExpired)); -} - -// ---- ensure_trailing_slash / URL join tests ---- - -#[test] -fn test_ensure_trailing_slash_adds_slash() { - let url = Url::parse("http://localhost:3001").unwrap(); - let result = ensure_trailing_slash(url); - assert_eq!(result.as_str(), "http://localhost:3001/"); -} - -#[test] -fn test_ensure_trailing_slash_preserves_existing() { - let url = Url::parse("http://localhost:3001/").unwrap(); - let result = ensure_trailing_slash(url); - assert_eq!(result.as_str(), "http://localhost:3001/"); -} - -#[test] -fn test_ensure_trailing_slash_with_path() { - let url = Url::parse("http://localhost:3001/api/v1").unwrap(); - let result = ensure_trailing_slash(url); - assert_eq!(result.as_str(), "http://localhost:3001/api/v1/"); -} - -#[test] -fn test_relative_join_preserves_base_path() { - let base = ensure_trailing_slash(Url::parse("http://localhost:3001/api/v1").unwrap()); - let joined = base.join("oauth/device/code").unwrap(); - assert_eq!( - joined.as_str(), - "http://localhost:3001/api/v1/oauth/device/code" - ); -} - -#[test] -fn test_relative_join_on_root_url() { - let base = ensure_trailing_slash(Url::parse("http://localhost:3001").unwrap()); - let joined = base.join("oauth/device/code").unwrap(); - assert_eq!(joined.as_str(), "http://localhost:3001/oauth/device/code"); -} - -#[tokio::test] -async fn test_pending_device_code_debug_does_not_leak() { - let dir = TempDir::new().unwrap(); - let mut mocks = MockSet::new(); - mock_code_endpoint(&mut mocks); - let server = start_server(mocks).await; - - let pending = begin_pending(&server, &dir).await; - let debug = format!("{:?}", pending); - - assert!( - !debug.contains("test_device_code"), - "PendingDeviceCode Debug should not contain the device code, got: {debug}" - ); -} diff --git a/vendor/stack-auth/src/lib.rs b/vendor/stack-auth/src/lib.rs deleted file mode 100644 index 7dd91d2e..00000000 --- a/vendor/stack-auth/src/lib.rs +++ /dev/null @@ -1,273 +0,0 @@ -#![doc(html_favicon_url = "https://cipherstash.com/favicon.ico")] -#![doc = include_str!("../README.md")] -// Security lints -#![deny(unsafe_code)] -#![warn(clippy::unwrap_used)] -#![warn(clippy::expect_used)] -#![warn(clippy::panic)] -// Prevent mem::forget from bypassing ZeroizeOnDrop -#![warn(clippy::mem_forget)] -// Prevent accidental data leaks via output -#![warn(clippy::print_stdout)] -#![warn(clippy::print_stderr)] -#![warn(clippy::dbg_macro)] -// Code quality -#![warn(unreachable_pub)] -#![warn(unused_results)] -#![warn(clippy::todo)] -#![warn(clippy::unimplemented)] -// Relax in tests -#![cfg_attr(test, allow(clippy::unwrap_used))] -#![cfg_attr(test, allow(clippy::expect_used))] -#![cfg_attr(test, allow(clippy::panic))] -#![cfg_attr(test, allow(unused_results))] - -use std::convert::Infallible; -use std::future::Future; -#[cfg(not(any(test, feature = "test-utils")))] -use std::time::Duration; - -use vitaminc::protected::OpaqueDebug; -use zeroize::ZeroizeOnDrop; - -mod access_key; -mod access_key_refresher; -mod access_key_strategy; -mod auto_refresh; -mod auto_strategy; -mod device_client; -mod device_code; -mod oauth_refresher; -mod oauth_strategy; -mod refresher; -mod service_token; -mod token; - -#[cfg(any(test, feature = "test-utils"))] -mod static_token_strategy; - -pub use access_key::{AccessKey, InvalidAccessKey}; -pub use access_key_strategy::{AccessKeyStrategy, AccessKeyStrategyBuilder}; -pub use auto_strategy::{AutoStrategy, AutoStrategyBuilder}; -pub use device_code::{DeviceCodeStrategy, DeviceCodeStrategyBuilder, PendingDeviceCode}; -pub use oauth_strategy::{OAuthStrategy, OAuthStrategyBuilder}; -pub use service_token::ServiceToken; -#[cfg(any(test, feature = "test-utils"))] -pub use static_token_strategy::StaticTokenStrategy; -pub use token::Token; - -pub use device_client::{bind_client_device, DeviceClientError}; - -// Re-exports from stack-profile for backward compatibility. -pub use stack_profile::DeviceIdentity; - -/// A strategy for obtaining access tokens. -/// -/// Implementations handle all details of authentication, token caching, and -/// refresh. Callers just call [`get_token`](AuthStrategy::get_token) whenever -/// they need a valid token. -/// -/// The trait is designed to be implemented for `&T`, so that callers can use -/// shared references (e.g. `&OAuthStrategy`) without consuming the strategy. -/// -/// # Token refresh -/// -/// All strategies that cache tokens ([`AccessKeyStrategy`], [`OAuthStrategy`], -/// [`AutoStrategy`]) share the same internal refresh engine. Understanding the -/// refresh model helps predict how [`get_token`](AuthStrategy::get_token) -/// behaves under concurrent access. -/// -/// ## Expiry vs usability -/// -/// A token has two time thresholds: -/// -/// - **Expired** — the token is within **90 seconds** of its `expires_at` -/// timestamp. This triggers a preemptive refresh attempt. -/// - **Usable** — the token has **not yet reached** its `expires_at` timestamp. -/// A token can be "expired" (in the preemptive sense) but still "usable" -/// (the server will still accept it). -/// -/// ## Concurrent refresh strategies -/// -/// The gap between "expired" and "unusable" enables two refresh modes: -/// -/// 1. **Expiring but still usable** — The first caller triggers a background -/// refresh. Concurrent callers receive the current (still-valid) token -/// immediately without blocking. -/// 2. **Fully expired** — The first caller blocks while refreshing. Concurrent -/// callers wait until the refresh completes, then all receive the new token. -/// -/// Only one refresh runs at a time, regardless of how many callers request a -/// token concurrently. -/// -/// ## Flow diagram -/// -/// ```mermaid -/// flowchart TD -/// Start["get_token()"] --> Lock["Acquire lock"] -/// Lock --> Cached{Token cached?} -/// Cached -- No --> InitAuth["Authenticate -/// (lock held)"] -/// InitAuth -- OK --> ReturnNew["Return new token"] -/// InitAuth -- NotFound --> ErrNotFound["NotAuthenticated"] -/// InitAuth -- Err --> ErrAuth["Return error"] -/// Cached -- Yes --> CheckRefresh{Expired?} -/// -/// CheckRefresh -- "No (fresh)" --> ReturnOk["Return cached token"] -/// -/// CheckRefresh -- "Yes (needs refresh)" --> InProgress{Refresh in progress?} -/// InProgress -- Yes --> WaitOrReturn["Return token if usable, -/// else wait for refresh"] -/// WaitOrReturn -- OK --> ReturnOk -/// WaitOrReturn -- "refresh failed" --> ErrExpired["TokenExpired"] -/// -/// InProgress -- No --> HasCred{Refresh credential?} -/// HasCred -- None --> CheckUsable["Return token if usable, -/// else TokenExpired"] -/// -/// HasCred -- Yes --> Usable{Still usable?} -/// -/// Usable -- "Yes (preemptive)" --> NonBlocking["Refresh in background -/// (lock released)"] -/// NonBlocking --> ReturnOld["Return current token"] -/// -/// Usable -- "No (fully expired)" --> Blocking["Refresh -/// (lock held)"] -/// Blocking -- OK --> ReturnNew2["Return new token"] -/// Blocking -- Err --> ErrExpired["TokenExpired"] -/// ``` -#[cfg_attr(doc, aquamarine::aquamarine)] -pub trait AuthStrategy: Send { - /// Retrieve a valid access token, refreshing or re-authenticating as needed. - fn get_token(self) -> impl Future> + Send; -} - -/// A sensitive token string that is zeroized on drop and hidden from debug output. -/// -/// `SecretToken` wraps a `String` and enforces two invariants: -/// -/// - **Zeroized on drop**: the backing memory is overwritten with zeros when -/// the token goes out of scope, preventing it from lingering in memory. -/// - **Opaque debug**: the [`Debug`] implementation prints `"***"` instead of -/// the actual value, so tokens won't leak into logs or error messages. -/// -/// Use [`SecretToken::new`] to wrap a string value (e.g. an access key -/// loaded from configuration or an environment variable). -#[derive(Clone, OpaqueDebug, ZeroizeOnDrop, serde::Deserialize, serde::Serialize)] -#[serde(transparent)] -pub struct SecretToken(String); - -impl SecretToken { - /// Create a new `SecretToken` from a string value. - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } - - /// Expose the inner token string for FFI boundaries. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -/// Errors that can occur during an authentication flow. -#[derive(Debug, thiserror::Error, miette::Diagnostic)] -#[non_exhaustive] -pub enum AuthError { - /// The HTTP request to the auth server failed (network error, timeout, etc.). - #[error("HTTP request failed: {0}")] - Request(#[from] reqwest::Error), - /// The user denied the authorization request. - #[error("Authorization was denied")] - AccessDenied, - /// The grant type was rejected by the server. - #[error("Invalid grant")] - InvalidGrant, - /// The client ID is not recognized. - #[error("Invalid client")] - InvalidClient, - /// A URL could not be parsed. - #[error("Invalid URL: {0}")] - InvalidUrl(#[from] url::ParseError), - /// The requested region is not supported. - #[error("Unsupported region: {0}")] - Region(#[from] cts_common::RegionError), - /// The workspace CRN could not be parsed. - #[error("Invalid workspace CRN: {0}")] - InvalidCrn(cts_common::InvalidCrn), - /// An access key was provided but the workspace CRN is missing. - /// - /// Set the `CS_WORKSPACE_CRN` environment variable or call - /// [`AutoStrategyBuilder::with_workspace_crn`](crate::AutoStrategyBuilder::with_workspace_crn). - #[error("Workspace CRN is required when using an access key — set CS_WORKSPACE_CRN or call AutoStrategyBuilder::with_workspace_crn")] - MissingWorkspaceCrn, - /// No credentials are available (e.g. not logged in, no access key configured). - #[error("Not authenticated")] - NotAuthenticated, - /// A token (access token or device code) has expired. - #[error("Token expired")] - TokenExpired, - /// The access key string is malformed (e.g. missing `CSAK` prefix or `.` separator). - #[error("Invalid access key: {0}")] - InvalidAccessKey(#[from] access_key::InvalidAccessKey), - /// The JWT could not be decoded or its claims are malformed. - #[error("Invalid token: {0}")] - InvalidToken(String), - /// An unexpected error was returned by the auth server. - #[error("Server error: {0}")] - Server(String), - /// A token store operation failed. - #[error("Token store error: {0}")] - Store(#[from] stack_profile::ProfileError), -} - -impl From for AuthError { - fn from(never: Infallible) -> Self { - match never {} - } -} - -/// Read the `CS_CTS_HOST` environment variable and parse it as a URL. -/// -/// Returns `Ok(None)` if the variable is not set or empty. -/// Returns `Ok(Some(url))` if the variable is set and valid. -/// Returns `Err(_)` if the variable is set but not a valid URL. -pub(crate) fn cts_base_url_from_env() -> Result, AuthError> { - match std::env::var("CS_CTS_HOST") { - Ok(val) if !val.is_empty() => Ok(Some(val.parse()?)), - _ => Ok(None), - } -} - -/// Ensure a URL has a trailing slash so that `Url::join` with relative paths -/// appends to the path rather than replacing the last segment. -pub(crate) fn ensure_trailing_slash(mut url: url::Url) -> url::Url { - if !url.path().ends_with('/') { - url.set_path(&format!("{}/", url.path())); - } - url -} - -/// Create a [`reqwest::Client`] with standard timeouts. -/// -/// In test builds, timeouts are omitted so that `tokio::test(start_paused = true)` -/// does not auto-advance time past the connect timeout before the mock server -/// can respond. -pub(crate) fn http_client() -> reqwest::Client { - #[cfg(any(test, feature = "test-utils"))] - { - reqwest::Client::builder() - .pool_max_idle_per_host(10) - .build() - .unwrap_or_else(|_| reqwest::Client::new()) - } - #[cfg(not(any(test, feature = "test-utils")))] - { - reqwest::Client::builder() - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) - .pool_idle_timeout(Duration::from_secs(5)) - .pool_max_idle_per_host(10) - .build() - .unwrap_or_else(|_| reqwest::Client::new()) - } -} diff --git a/vendor/stack-auth/src/oauth_refresher.rs b/vendor/stack-auth/src/oauth_refresher.rs deleted file mode 100644 index 23425b03..00000000 --- a/vendor/stack-auth/src/oauth_refresher.rs +++ /dev/null @@ -1,73 +0,0 @@ -use url::Url; - -use stack_profile::ProfileStore; - -use crate::refresher::Refresher; -use crate::{AuthError, SecretToken, Token}; - -/// Implements [`Refresher`] using OAuth refresh tokens. -/// -/// Optionally owns a [`ProfileStore`] for persisting refreshed tokens to disk. -/// When the store is `None`, tokens are cached in memory only. -pub(crate) struct OAuthRefresher { - store: Option, - base_url: Url, - client_id: String, - region: String, - device_instance_id: Option, -} - -impl OAuthRefresher { - pub(crate) fn new( - store: Option, - base_url: Url, - client_id: impl Into, - region: impl Into, - device_instance_id: Option, - ) -> Self { - Self { - store, - base_url, - client_id: client_id.into(), - region: region.into(), - device_instance_id, - } - } -} - -impl Refresher for OAuthRefresher { - type Credential = SecretToken; - - fn save(&self, token: &Token) { - if let Some(store) = &self.store { - match store.save_profile(token) { - Ok(()) => tracing::debug!("refreshed token saved to disk"), - Err(err) => tracing::warn!(%err, "failed to save refreshed token to disk"), - } - } - } - - fn try_credential(&self, token: Option<&mut Token>) -> Option { - token.and_then(|t| t.take_refresh_token()) - } - - fn restore(&self, token: &mut Token, credential: Self::Credential) { - token.refresh_token = Some(credential); - } - - async fn refresh(&self, credential: &Self::Credential) -> Result { - let mut token = Token::refresh( - credential, - &self.base_url, - &self.client_id, - self.device_instance_id.as_deref(), - ) - .await?; - token.set_region(&self.region); - token.set_client_id(&self.client_id); - if let Some(ref id) = self.device_instance_id { - token.set_device_instance_id(id); - } - Ok(token) - } -} diff --git a/vendor/stack-auth/src/oauth_strategy.rs b/vendor/stack-auth/src/oauth_strategy.rs deleted file mode 100644 index 4b28e44c..00000000 --- a/vendor/stack-auth/src/oauth_strategy.rs +++ /dev/null @@ -1,196 +0,0 @@ -use cts_common::{Crn, CtsServiceDiscovery, Region, ServiceDiscovery}; -use tracing::warn; - -use stack_profile::ProfileStore; - -use crate::auto_refresh::AutoRefresh; -use crate::oauth_refresher::OAuthRefresher; -use crate::{ensure_trailing_slash, AuthError, AuthStrategy, ServiceToken, Token}; - -/// An [`AuthStrategy`] that uses OAuth refresh tokens to maintain a valid access token. -/// -/// # Construction -/// -/// Use [`OAuthStrategy::with_token`] with a token obtained from a device code flow -/// (or any other OAuth flow) for in-memory caching only. Use -/// [`OAuthStrategy::with_profile`] to load a token from disk and persist -/// refreshed tokens back to the store. -/// -/// # Example -/// -/// ```no_run -/// use stack_auth::{OAuthStrategy, Token}; -/// use cts_common::Region; -/// -/// # fn run(token: Token) -> Result<(), Box> { -/// let region = Region::aws("ap-southeast-2")?; -/// let strategy = OAuthStrategy::with_token(region, "my-client-id", token).build()?; -/// # Ok(()) -/// # } -/// ``` -pub struct OAuthStrategy { - crn: Option, - inner: AutoRefresh, -} - -impl OAuthStrategy { - /// Return a builder for configuring an `OAuthStrategy` from a token. - /// - /// The token's `region` and `client_id` fields are set before caching. - /// No token store is used — tokens are not persisted to disk. - pub fn with_token( - region: Region, - client_id: impl Into, - token: Token, - ) -> OAuthStrategyBuilder { - OAuthStrategyBuilder { - source: OAuthTokenSource::Token { - region, - client_id: client_id.into(), - token, - }, - base_url_override: None, - } - } - - /// Return a builder for configuring an `OAuthStrategy` from a profile store. - /// - /// The token is loaded from the store when [`OAuthStrategyBuilder::build`] is called. - /// The builder allows further configuration (e.g. overriding the base URL) before building. - /// - /// The token must have `region` and `client_id` set (as saved by - /// [`DeviceCodeStrategy`](crate::DeviceCodeStrategy) or a prior - /// `OAuthStrategy`). The store is used for persisting refreshed tokens. - pub fn with_profile(store: ProfileStore) -> OAuthStrategyBuilder { - OAuthStrategyBuilder { - source: OAuthTokenSource::Store(store), - base_url_override: None, - } - } - - /// Return the workspace CRN, if one was extracted from the token at build time. - pub fn workspace_crn(&self) -> Option<&Crn> { - self.crn.as_ref() - } -} - -impl AuthStrategy for &OAuthStrategy { - async fn get_token(self) -> Result { - Ok(self.inner.get_token().await?) - } -} - -/// Where the initial OAuth token comes from. -enum OAuthTokenSource { - /// A token provided directly (in-memory only, no store). - Token { - region: Region, - client_id: String, - token: Token, - }, - /// A token loaded from a persistent store. - Store(ProfileStore), -} - -/// Builder for [`OAuthStrategy`]. -/// -/// Created via [`OAuthStrategy::with_token`] or [`OAuthStrategy::with_profile`]. -pub struct OAuthStrategyBuilder { - source: OAuthTokenSource, - base_url_override: Option, -} - -impl OAuthStrategyBuilder { - /// Override the base URL resolved by service discovery. - /// - /// Useful for pointing at a local or mock auth server during testing. - #[cfg(any(test, feature = "test-utils"))] - pub fn base_url(mut self, url: url::Url) -> Self { - self.base_url_override = Some(url); - self - } - - /// Build the [`OAuthStrategy`]. - /// - /// Resolves the base URL via service discovery unless overridden with - /// `base_url` (available when the `test-utils` feature is enabled). - pub fn build(self) -> Result { - match self.source { - OAuthTokenSource::Token { - region, - client_id, - mut token, - } => { - let base_url = match self.base_url_override { - Some(url) => url, - None => crate::cts_base_url_from_env()? - .unwrap_or(CtsServiceDiscovery::endpoint(region)?), - }; - // Derive CRN from the explicit region parameter and the token's - // workspace claim. We can't use token.workspace_crn() here - // because set_region() hasn't been called on the token yet. - let crn = token - .workspace_id() - .map(|ws| Crn::new(region, ws)) - .map_err(|e| { - warn!("Could not extract workspace CRN from token: {e}"); - e - }) - .ok(); - let region_id = region.identifier(); - let device_instance_id = token.device_instance_id().map(String::from); - token.set_region(®ion_id); - token.set_client_id(&client_id); - let refresher = OAuthRefresher::new( - None, - ensure_trailing_slash(base_url), - &client_id, - ®ion_id, - device_instance_id, - ); - Ok(OAuthStrategy { - crn, - inner: AutoRefresh::with_token(refresher, token), - }) - } - OAuthTokenSource::Store(store) => { - let ws_store = store.current_workspace_store()?; - let token: Token = ws_store.load_profile()?; - - let region_str = token - .region() - .ok_or(AuthError::NotAuthenticated)? - .to_string(); - let client_id = token - .client_id() - .ok_or(AuthError::NotAuthenticated)? - .to_string(); - let crn = token - .workspace_crn() - .map_err(|e| { - warn!("Could not extract workspace CRN from token: {e}"); - e - }) - .ok(); - let device_instance_id = token.device_instance_id().map(String::from); - - let base_url = match self.base_url_override { - Some(url) => url, - None => crate::cts_base_url_from_env()?.unwrap_or(token.issuer()?), - }; - - let refresher = OAuthRefresher::new( - Some(ws_store), - ensure_trailing_slash(base_url), - &client_id, - ®ion_str, - device_instance_id, - ); - Ok(OAuthStrategy { - crn, - inner: AutoRefresh::with_token(refresher, token), - }) - } - } - } -} diff --git a/vendor/stack-auth/src/refresher.rs b/vendor/stack-auth/src/refresher.rs deleted file mode 100644 index 576e11a4..00000000 --- a/vendor/stack-auth/src/refresher.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::future::Future; - -use crate::{AuthError, Token}; - -/// Internal trait defining how to refresh or re-authenticate to obtain a new [`Token`]. -/// -/// [`AutoRefresh`](crate::auto_refresh::AutoRefresh) delegates the type-specific -/// parts of token refresh to the `Refresher` implementation while handling the -/// concurrency orchestration (cascade prevention, two-tier locking) generically. -pub(crate) trait Refresher: Send + Sync { - /// The credential extracted from the current token before a refresh attempt. - type Credential: Send; - - /// Persist a token after a successful refresh. Best-effort — implementations - /// should log on failure rather than returning an error. - fn save(&self, token: &Token); - - /// Extract a credential for refreshing. - /// - /// `token` is `None` on cold start (no cached token). Returns `None` if - /// this refresher can't produce a token without a prior one (e.g. OAuth - /// needs a refresh token). - fn try_credential(&self, token: Option<&mut Token>) -> Option; - - /// Restore state after a failed refresh attempt (e.g. put the refresh token - /// back so the next caller can retry). - fn restore(&self, token: &mut Token, credential: Self::Credential); - - /// Perform the HTTP refresh or authentication call. - fn refresh( - &self, - credential: &Self::Credential, - ) -> impl Future> + Send; -} diff --git a/vendor/stack-auth/src/service_token.rs b/vendor/stack-auth/src/service_token.rs deleted file mode 100644 index 90a6273d..00000000 --- a/vendor/stack-auth/src/service_token.rs +++ /dev/null @@ -1,378 +0,0 @@ -use cts_common::claims::{ServiceType, Services}; -use cts_common::WorkspaceId; -use url::Url; -use vitaminc::protected::OpaqueDebug; -use zeroize::ZeroizeOnDrop; - -use crate::{AuthError, SecretToken}; - -/// A CipherStash service token returned by an [`AuthStrategy`](crate::AuthStrategy). -/// -/// Wraps a bearer credential ([`SecretToken`]) together with eagerly decoded -/// JWT claims that are used for service discovery. The JWT is decoded (but -/// **not** signature-verified) using [`cts_common::claims::Claims`], so only -/// CipherStash-issued service tokens (from CTS or the access-key exchange) -/// will have their claims resolved. -/// -/// # Decoded claims -/// -/// * `subject()` — the `sub` claim (e.g. `"CS|auth0|user123"`). -/// * `workspace_id()` — the workspace identifier from the token. -/// * `issuer()` — the `iss` URL, i.e. the CTS host for this workspace. -/// * `zerokms_url()` — the ZeroKMS endpoint from the `services` claim. -/// -/// For non-JWT tokens (e.g. static test tokens) or JWTs that don't match -/// the CipherStash claims schema, these methods return -/// `Err(AuthError::InvalidToken)`. -/// -/// # Security -/// -/// Like [`SecretToken`], this is zeroized on drop and hidden from [`Debug`] -/// output. -#[derive(Clone, OpaqueDebug, ZeroizeOnDrop)] -pub struct ServiceToken { - secret: SecretToken, - #[zeroize(skip)] - decoded: Result, -} - -#[derive(Clone, Debug)] -struct DecodedClaims { - subject: String, - workspace: WorkspaceId, - issuer: Url, - services: Services, -} - -impl ServiceToken { - /// Create a `ServiceToken` from a [`SecretToken`]. - /// - /// If the token string is a valid JWT with `iss` and `services` claims, - /// they are decoded eagerly. If decoding fails (not a JWT, missing claims, - /// etc.) the token is still usable as a bearer credential — `issuer()` and - /// `zerokms_url()` will simply return an error. - pub fn new(secret: SecretToken) -> Self { - let decoded = Self::try_decode(&secret); - Self { secret, decoded } - } - - /// Expose the inner token string for use as a bearer credential. - pub fn as_str(&self) -> &str { - self.secret.as_str() - } - - /// Return the `sub` (subject) claim from the JWT. - /// - /// In CipherStash tokens the subject encodes the principal identity, - /// e.g. `"CS|auth0|user123"` for a user or `"CS|CSAKkeyId"` for an - /// access key. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidToken`] if the token is not a valid JWT or - /// the claims could not be decoded. - pub fn subject(&self) -> Result<&str, AuthError> { - self.decoded - .as_ref() - .map(|d| d.subject.as_str()) - .map_err(|reason| AuthError::InvalidToken(reason.clone())) - } - - /// Return the workspace identifier from the JWT claims. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidToken`] if the token is not a valid JWT or - /// the claims could not be decoded. - pub fn workspace_id(&self) -> Result<&WorkspaceId, AuthError> { - self.decoded - .as_ref() - .map(|d| &d.workspace) - .map_err(|reason| AuthError::InvalidToken(reason.clone())) - } - - /// Return the `iss` (issuer) URL from the JWT claims. - /// - /// In CipherStash tokens the issuer is the CTS host URL for the workspace. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidToken`] if the token is not a valid JWT or - /// the `iss` claim could not be parsed as a URL. - pub fn issuer(&self) -> Result<&Url, AuthError> { - self.decoded - .as_ref() - .map(|d| &d.issuer) - .map_err(|reason| AuthError::InvalidToken(reason.clone())) - } - - /// Return the decoded services map from the JWT claims. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidToken`] if the token is not a valid JWT or - /// the claims could not be decoded. - pub fn services(&self) -> Result<&Services, AuthError> { - self.decoded - .as_ref() - .map(|d| &d.services) - .map_err(|reason| AuthError::InvalidToken(reason.clone())) - } - - /// Return the ZeroKMS endpoint URL from the `services` claim. - /// - /// CTS-issued JWTs include a `services` claim containing a map of service - /// type to endpoint URL. This method looks up the `zerokms` entry. - /// - /// # Errors - /// - /// Returns [`AuthError::InvalidToken`] if the token is not a valid JWT or - /// the `services` claim does not include a ZeroKMS endpoint. - pub fn zerokms_url(&self) -> Result { - self.services()? - .get(ServiceType::ZeroKms) - .cloned() - .ok_or_else(|| { - AuthError::InvalidToken( - "Token does not include a ZeroKMS endpoint in the services claim".into(), - ) - }) - } - - /// Attempt to decode the JWT claims from the token string. - /// - /// NOTE: This does not verify the token signature or validate any claims, - /// it only decodes the claims if the token is a well-formed JWT. - fn try_decode(secret: &SecretToken) -> Result { - use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; - use std::collections::HashSet; - - let token_str = secret.as_str(); - let header = - decode_header(token_str).map_err(|e| format!("failed to decode JWT header: {e}"))?; - - let dummy_key = DecodingKey::from_secret(&[]); - let mut validation = Validation::new(header.alg); - validation.validate_exp = false; - validation.validate_aud = false; - validation.required_spec_claims = HashSet::new(); - validation.insecure_disable_signature_validation(); - - let data: jsonwebtoken::TokenData = - decode(token_str, &dummy_key, &validation) - .map_err(|e| format!("failed to decode JWT claims: {e}"))?; - - let issuer: Url = data - .claims - .iss - .parse() - .map_err(|e| format!("iss claim is not a valid URL: {e}"))?; - - Ok(DecodedClaims { - subject: data.claims.sub, - workspace: data.claims.workspace, - issuer, - services: data.claims.services, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::collections::BTreeMap; - - fn make_jwt(iss: &str, services: Option>) -> String { - use jsonwebtoken::{encode, EncodingKey, Header}; - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - let mut claims = serde_json::json!({ - "iss": iss, - "sub": "CS|test-user", - "aud": "legacy-aud-value", - "iat": now, - "exp": now + 3600, - "workspace": "ZVATKW3VHMFG27DY", - "scope": "", - }); - - if let Some(svc) = services { - claims["services"] = serde_json::to_value(svc).unwrap(); - } - - encode( - &Header::default(), - &claims, - &EncodingKey::from_secret(b"test-secret"), - ) - .unwrap() - } - - fn services_with_zerokms(url: &str) -> Option> { - Some(BTreeMap::from([("zerokms", url)])) - } - - #[test] - fn jwt_token_provides_issuer() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt.clone())); - - assert_eq!(token.as_str(), jwt); - assert_eq!(token.issuer().unwrap().as_str(), "https://cts.example.com/"); - } - - #[test] - fn non_jwt_token_returns_errors_with_reason() { - let token = ServiceToken::new(SecretToken::new("not-a-jwt")); - - assert_eq!(token.as_str(), "not-a-jwt"); - - let err = token.issuer().unwrap_err().to_string(); - assert!( - err.contains("failed to decode JWT header"), - "expected specific decode error, got: {err}" - ); - } - - #[test] - fn zerokms_url_from_services_claim() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt)); - assert_eq!( - token.zerokms_url().unwrap().as_str(), - "https://zerokms.example.com/" - ); - } - - #[test] - fn zerokms_url_from_services_claim_localhost() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("http://localhost:3002/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt)); - assert_eq!( - token.zerokms_url().unwrap().as_str(), - "http://localhost:3002/" - ); - } - - #[test] - fn zerokms_url_errors_when_services_claim_missing() { - let jwt = make_jwt("https://cts.example.com/", None); - let token = ServiceToken::new(SecretToken::new(jwt)); - let err = token.zerokms_url().unwrap_err().to_string(); - assert!( - err.contains("services claim"), - "expected services claim error, got: {err}" - ); - } - - #[test] - fn zerokms_url_errors_for_non_jwt() { - let token = ServiceToken::new(SecretToken::new("not-a-jwt")); - assert!(token.zerokms_url().is_err()); - } - - #[test] - fn services_returns_map_for_valid_jwt() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt)); - let services = token.services().unwrap(); - assert_eq!( - services - .get(cts_common::claims::ServiceType::ZeroKms) - .map(|u| u.as_str()), - Some("https://zerokms.example.com/") - ); - } - - #[test] - fn services_returns_empty_map_when_claim_missing() { - let jwt = make_jwt("https://cts.example.com/", None); - let token = ServiceToken::new(SecretToken::new(jwt)); - let services = token.services().unwrap(); - assert!(services.is_empty()); - } - - #[test] - fn services_errors_for_non_jwt() { - let token = ServiceToken::new(SecretToken::new("not-a-jwt")); - let err = token.services().unwrap_err().to_string(); - assert!( - err.contains("failed to decode JWT header"), - "expected specific decode error, got: {err}" - ); - } - - #[test] - fn subject_from_valid_jwt() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt)); - assert_eq!( - token.subject().unwrap(), - "CS|test-user", - "subject should match JWT sub claim" - ); - } - - #[test] - fn subject_errors_for_non_jwt() { - let token = ServiceToken::new(SecretToken::new("not-a-jwt")); - assert!( - token.subject().is_err(), - "subject should error for non-JWT token" - ); - } - - #[test] - fn workspace_id_from_valid_jwt() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt)); - assert_eq!( - token.workspace_id().unwrap().to_string(), - "ZVATKW3VHMFG27DY", - "workspace_id should match JWT workspace claim" - ); - } - - #[test] - fn workspace_id_errors_for_non_jwt() { - let token = ServiceToken::new(SecretToken::new("not-a-jwt")); - assert!( - token.workspace_id().is_err(), - "workspace_id should error for non-JWT token" - ); - } - - #[test] - fn debug_does_not_leak_secret() { - let jwt = make_jwt( - "https://cts.example.com/", - services_with_zerokms("https://zerokms.example.com/"), - ); - let token = ServiceToken::new(SecretToken::new(jwt.clone())); - let debug = format!("{:?}", token); - assert!(!debug.contains(&jwt)); - } -} diff --git a/vendor/stack-auth/src/static_token_strategy.rs b/vendor/stack-auth/src/static_token_strategy.rs deleted file mode 100644 index 66b86f69..00000000 --- a/vendor/stack-auth/src/static_token_strategy.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{AuthError, AuthStrategy, SecretToken, ServiceToken}; - -/// A simple [`AuthStrategy`] that always returns a fixed token. -/// -/// Useful in tests where a token has already been obtained (e.g. from a mock auth -/// server or via federation) and just needs to be presented as-is. -/// -/// ``` -/// use stack_auth::{StaticTokenStrategy, AuthStrategy}; -/// -/// # async fn example() { -/// let strategy = StaticTokenStrategy::new("my-token"); -/// let token = (&strategy).get_token().await.unwrap(); -/// assert_eq!(token.as_str(), "my-token"); -/// # } -/// ``` -pub struct StaticTokenStrategy(SecretToken); - -impl StaticTokenStrategy { - /// Create a new `StaticTokenStrategy` wrapping the given token string. - pub fn new(token: impl Into) -> Self { - Self(SecretToken::new(token)) - } -} - -impl AuthStrategy for &StaticTokenStrategy { - async fn get_token(self) -> Result { - Ok(ServiceToken::new(self.0.clone())) - } -} diff --git a/vendor/stack-auth/src/token.rs b/vendor/stack-auth/src/token.rs deleted file mode 100644 index 0107a711..00000000 --- a/vendor/stack-auth/src/token.rs +++ /dev/null @@ -1,577 +0,0 @@ -use std::time::{SystemTime, UNIX_EPOCH}; - -use cts_common::claims::Claims; -use cts_common::{Crn, Region, WorkspaceId}; -use url::Url; - -use crate::{http_client, AuthError, SecretToken}; - -impl stack_profile::ProfileData for Token { - const FILENAME: &'static str = "auth.json"; - const MODE: Option = Some(0o600); -} - -/// How many seconds before expiry [`Token::is_expired`] returns `true`. -/// -/// This leeway triggers preemptive refresh well before the token becomes -/// unusable, giving the HTTP refresh call time to complete while concurrent -/// callers can still use the current token. -const EXPIRY_LEEWAY_SECS: u64 = 90; - -/// An access token returned by a successful authentication flow. -/// -/// The token contains a [`SecretToken`] (the bearer credential), a token type -/// (typically `"Bearer"`), and an absolute expiry timestamp. -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct Token { - pub(crate) access_token: SecretToken, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) refresh_token: Option, - pub(crate) token_type: String, - pub(crate) expires_at: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) client_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub(crate) device_instance_id: Option, -} - -impl Token { - /// Returns a reference to the access token credential. - /// - /// The returned [`SecretToken`] is opaque — its [`Debug`] output is masked. - /// Pass it to API clients that need the raw bearer token. - pub fn access_token(&self) -> &SecretToken { - &self.access_token - } - - /// The token type (e.g. `"Bearer"`). - pub fn token_type(&self) -> &str { - &self.token_type - } - - /// The absolute epoch timestamp when the token expires. - pub fn expires_at(&self) -> u64 { - self.expires_at - } - - /// How many seconds until the token expires (computed from the current time). - pub fn expires_in(&self) -> u64 { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - self.expires_at.saturating_sub(now) - } - - /// Returns `true` if the token has expired (with 90 seconds of leeway). - /// - /// The 90-second leeway triggers preemptive refresh well before the token - /// becomes unusable, giving the HTTP refresh call plenty of time to complete - /// while the current token is still valid for concurrent callers. - /// - /// For checking whether the token is still usable as a bearer credential, - /// use [`is_usable`](Self::is_usable) instead. - pub fn is_expired(&self) -> bool { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - now + EXPIRY_LEEWAY_SECS >= self.expires_at - } - - /// Returns `true` if the token is still usable (before the actual expiry timestamp). - /// - /// Unlike [`is_expired`](Self::is_expired) which includes 90s leeway for preemptive - /// refresh, this only returns `false` when the token has genuinely expired. - pub fn is_usable(&self) -> bool { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - now < self.expires_at - } - - /// Returns a reference to the refresh token, if one was provided. - pub fn refresh_token(&self) -> Option<&SecretToken> { - self.refresh_token.as_ref() - } - - /// Takes the refresh token out, leaving `None` in its place. - pub fn take_refresh_token(&mut self) -> Option { - self.refresh_token.take() - } - - /// Returns the stored region identifier, if any. - pub fn region(&self) -> Option<&str> { - self.region.as_deref() - } - - /// Returns the stored client ID, if any. - pub fn client_id(&self) -> Option<&str> { - self.client_id.as_deref() - } - - /// Set the region identifier on this token. - pub(crate) fn set_region(&mut self, region: impl Into) { - self.region = Some(region.into()); - } - - /// Set the client ID on this token. - pub(crate) fn set_client_id(&mut self, client_id: impl Into) { - self.client_id = Some(client_id.into()); - } - - /// Returns the stored device instance ID, if any. - pub fn device_instance_id(&self) -> Option<&str> { - self.device_instance_id.as_deref() - } - - /// Set the device instance ID on this token. - pub(crate) fn set_device_instance_id(&mut self, id: impl Into) { - self.device_instance_id = Some(id.into()); - } - - /// Returns the workspace ID from the JWT claims. - /// - /// The access token is decoded (without signature verification) to extract - /// the `workspace` claim. - pub fn workspace_id(&self) -> Result { - self.decode_claims().map(|c| c.workspace) - } - - /// Returns the workspace CRN derived from the token's region and workspace ID. - /// - /// The region is set during the device code flow, and the workspace ID is - /// extracted from the JWT `workspace` claim. - pub fn workspace_crn(&self) -> Result { - let workspace_id = self.workspace_id()?; - let region: Region = self - .region() - .ok_or(AuthError::NotAuthenticated)? - .parse() - .map_err(|e: cts_common::RegionError| AuthError::Server(e.to_string()))?; - Ok(Crn::new(region, workspace_id)) - } - - /// Returns the issuer URL from the JWT claims. - /// - /// The `iss` claim in CipherStash tokens is the CTS host URL for the - /// workspace, so this can be used directly as the CTS base URL. - pub fn issuer(&self) -> Result { - let claims = self.decode_claims()?; - claims.iss.parse().map_err(AuthError::from) - } - - /// Decode the JWT payload into [`Claims`] without verifying the signature. - /// - /// This is safe because we already possess the token — we just need to read - /// the claims it contains. - fn decode_claims(&self) -> Result { - use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; - use std::collections::HashSet; - - let token_str = self.access_token.as_str(); - let header = decode_header(token_str) - .map_err(|e| AuthError::InvalidToken(format!("invalid JWT header: {e}")))?; - - let dummy_key = DecodingKey::from_secret(&[]); - let mut validation = Validation::new(header.alg); - validation.validate_exp = false; - validation.validate_aud = false; - validation.required_spec_claims = HashSet::new(); - validation.insecure_disable_signature_validation(); - - decode(token_str, &dummy_key, &validation) - .map(|data| data.claims) - .map_err(|e| AuthError::InvalidToken(format!("failed to decode JWT claims: {e}"))) - } - - /// Exchange a refresh token for a new [`Token`] via the `/oauth/token` - /// endpoint. - /// - /// This is a static constructor — it takes a bare [`SecretToken`] (the - /// refresh token) rather than operating on an existing `Token`. This - /// allows callers to manage the refresh token lifecycle independently - /// (e.g. taking it out of a cached token for cascade prevention and - /// restoring it on failure). - /// - /// # Errors - /// - /// - [`AuthError::InvalidGrant`] — the refresh token was revoked or expired. - /// - [`AuthError::InvalidClient`] — the client ID is not recognized. - /// - [`AuthError::Request`] — a network error occurred. - pub async fn refresh( - refresh_token: &SecretToken, - base_url: &Url, - client_id: &str, - device_instance_id: Option<&str>, - ) -> Result { - let token_url = base_url.join("oauth/token")?; - - tracing::debug!(url = %token_url, "refreshing token"); - - let resp = http_client() - .post(token_url) - .form(&RefreshRequest { - grant_type: "refresh_token", - client_id, - refresh_token: refresh_token.as_str(), - device_instance_id, - }) - .send() - .await?; - - if !resp.status().is_success() { - let err: RefreshErrorResponse = resp.json().await?; - tracing::debug!(error = %err.error, "token refresh failed"); - return Err(match err.error.as_str() { - "invalid_grant" => AuthError::InvalidGrant, - "invalid_client" => AuthError::InvalidClient, - "access_denied" => AuthError::AccessDenied, - _ => AuthError::Server(err.error_description), - }); - } - - let token_resp: RefreshResponse = resp.json().await?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); - - Ok(Token { - access_token: token_resp.access_token, - token_type: token_resp.token_type, - expires_at: now + token_resp.expires_in, - refresh_token: token_resp.refresh_token, - region: None, - client_id: None, - // TODO(CIP-2793): The server should include device_instance_id in the - // refresh response. Until then, callers (e.g. OAuthRefresher) must - // re-attach it manually after refresh. - device_instance_id: None, - }) - } -} - -#[derive(serde::Serialize)] -struct RefreshRequest<'a> { - grant_type: &'a str, - client_id: &'a str, - refresh_token: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - device_instance_id: Option<&'a str>, -} - -#[derive(serde::Deserialize)] -struct RefreshResponse { - access_token: SecretToken, - token_type: String, - expires_in: u64, - #[serde(default)] - refresh_token: Option, -} - -#[derive(serde::Deserialize)] -struct RefreshErrorResponse { - error: String, - #[serde(default)] - error_description: String, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::AuthError; - use mocktail::prelude::*; - - fn make_token(expires_in: u64, refresh: bool) -> Token { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - Token { - access_token: SecretToken::new("test-access-token"), - token_type: "Bearer".to_string(), - expires_at: now + expires_in, - refresh_token: if refresh { - Some(SecretToken::new("test-refresh-token")) - } else { - None - }, - region: None, - client_id: None, - device_instance_id: None, - } - } - - fn refresh_response_json() -> serde_json::Value { - serde_json::json!({ - "access_token": "new-access-token", - "token_type": "Bearer", - "expires_in": 3600, - "refresh_token": "new-refresh-token" - }) - } - - fn error_json(error: &str) -> serde_json::Value { - serde_json::json!({ - "error": error, - "error_description": format!("{error} occurred") - }) - } - - async fn start_server(mocks: MockSet) -> MockServer { - let server = MockServer::new_http("token-refresh-test").with_mocks(mocks); - server.start().await.unwrap(); - server - } - - #[test] - fn test_secret_token_debug_does_not_leak() { - let token = SecretToken("super_secret_value".to_string()); - let debug = format!("{:?}", token); - assert!( - !debug.contains("super_secret_value"), - "SecretToken Debug should not contain the secret, got: {debug}" - ); - } - - // ---- refresh() tests ---- - - #[tokio::test] - async fn test_refresh_success() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(refresh_response_json()); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let refreshed = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap(); - - assert_eq!(refreshed.access_token().as_str(), "new-access-token"); - assert_eq!(refreshed.token_type(), "Bearer"); - assert_eq!( - refreshed.refresh_token().unwrap().as_str(), - "new-refresh-token" - ); - assert!(!refreshed.is_expired()); - assert!((3598..=3600).contains(&refreshed.expires_in())); - } - - #[tokio::test] - async fn test_refresh_invalid_grant() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("invalid_grant")); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let err = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::InvalidGrant)); - } - - #[tokio::test] - async fn test_refresh_invalid_client() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("invalid_client")); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let err = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::InvalidClient)); - } - - #[tokio::test] - async fn test_refresh_access_denied() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("access_denied")); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let err = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap_err(); - - assert!(matches!(err, AuthError::AccessDenied)); - } - - #[tokio::test] - async fn test_refresh_unknown_error() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.bad_request().json(error_json("something_unexpected")); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let err = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap_err(); - - assert!(matches!(&err, AuthError::Server(desc) if desc == "something_unexpected occurred")); - } - - #[tokio::test] - async fn test_refresh_response_without_new_refresh_token() { - let mut mocks = MockSet::new(); - mocks.mock(|when, then| { - when.post().path("/oauth/token"); - then.json(serde_json::json!({ - "access_token": "new-access-token", - "token_type": "Bearer", - "expires_in": 3600 - })); - }); - let server = start_server(mocks).await; - let base_url = server.url(""); - - let refresh_token = SecretToken::new("test-refresh-token"); - let refreshed = Token::refresh(&refresh_token, &base_url, "cli", None) - .await - .unwrap(); - - assert_eq!(refreshed.access_token().as_str(), "new-access-token"); - assert!(refreshed.refresh_token().is_none()); - } - - #[tokio::test] - async fn test_refresh_debug_does_not_leak_tokens() { - let token = make_token(3600, true); - let debug = format!("{:?}", token); - assert!( - !debug.contains("test-access-token"), - "Debug output should not contain access token, got: {debug}" - ); - assert!( - !debug.contains("test-refresh-token"), - "Debug output should not contain refresh token, got: {debug}" - ); - } - - // ---- decode_claims / workspace_id / issuer tests ---- - - /// Build a Token whose access_token is a real (unsigned) JWT containing the - /// given claims JSON. - fn make_jwt_token(claims_json: serde_json::Value) -> Token { - use jsonwebtoken::{encode, EncodingKey, Header}; - let jwt = encode( - &Header::default(), - &claims_json, - &EncodingKey::from_secret(b"test-secret"), - ) - .expect("failed to encode JWT"); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - - Token { - access_token: SecretToken::new(jwt), - token_type: "Bearer".to_string(), - expires_at: now + 3600, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - } - } - - fn valid_claims_json() -> serde_json::Value { - serde_json::json!({ - "workspace": "7366ITCXSAPCH5TN", - "iss": "https://cts.example.com", - "sub": "user-123", - "aud": "https://cts.example.com", - "iat": 1700000000u64, - "exp": 1700003600u64, - "scope": "dataset:create" - }) - } - - #[test] - fn test_workspace_id_extracts_from_jwt() { - let token = make_jwt_token(valid_claims_json()); - let ws = token.workspace_id().expect("should extract workspace ID"); - assert_eq!(ws.to_string(), "7366ITCXSAPCH5TN"); - } - - #[test] - fn test_issuer_extracts_url_from_jwt() { - let token = make_jwt_token(valid_claims_json()); - let issuer = token.issuer().expect("should extract issuer"); - assert_eq!(issuer.as_str(), "https://cts.example.com/"); - } - - #[test] - fn test_workspace_id_fails_on_invalid_jwt() { - let token = Token { - access_token: SecretToken::new("not-a-jwt"), - token_type: "Bearer".to_string(), - expires_at: 0, - refresh_token: None, - region: None, - client_id: None, - device_instance_id: None, - }; - let err = token.workspace_id().unwrap_err(); - assert!(matches!(err, AuthError::InvalidToken(_))); - } - - #[test] - fn test_issuer_fails_on_missing_claims() { - let token = make_jwt_token(serde_json::json!({"sub": "user-123"})); - let err = token.issuer().unwrap_err(); - assert!(matches!(err, AuthError::InvalidToken(_))); - } - - #[test] - fn test_workspace_crn_derives_from_region_and_workspace() { - let mut token = make_jwt_token(valid_claims_json()); - token.set_region("ap-southeast-2.aws"); - let crn = token.workspace_crn().expect("should derive workspace CRN"); - assert_eq!(crn.to_string(), "crn:ap-southeast-2.aws:7366ITCXSAPCH5TN"); - } - - #[test] - fn test_workspace_crn_fails_without_region() { - let token = make_jwt_token(valid_claims_json()); - let err = token.workspace_crn().unwrap_err(); - assert!(matches!(err, AuthError::NotAuthenticated)); - } - - #[test] - fn test_workspace_crn_fails_with_invalid_region() { - let mut token = make_jwt_token(valid_claims_json()); - token.set_region("invalid-region"); - let err = token.workspace_crn().unwrap_err(); - assert!(matches!(err, AuthError::Server(_))); - } -}