diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2a79efe3..2c45b8cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,10 @@ jobs: - name: Run Axum adapter tests run: cargo test-axum + - name: Run HTML processor benchmarks (smoke) + # -- --test runs each benchmark as a regular test (no timing harness) so CI stays fast + run: cargo bench -p trusted-server-core --bench html_processor_bench -- --test + test-cloudflare: name: cargo check (cloudflare native + wasm32-unknown-unknown) runs-on: ubuntu-latest @@ -101,6 +105,30 @@ jobs: - name: Run Cloudflare adapter tests (native host) run: cargo test-cloudflare + test-parity: + name: cargo test (cross-adapter parity) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Retrieve Rust version + id: rust-version + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ steps.rust-version.outputs.rust-version }} + components: clippy + cache-shared-key: cargo-${{ runner.os }} + + - name: Run cross-adapter parity tests + run: cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity + + - name: Clippy (parity test crate) + run: cargo clippy --manifest-path crates/integration-tests/Cargo.toml -- -D warnings + test-typescript: name: vitest runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 26b539f5..08271e5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3549,6 +3549,7 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", + "base64", "edgezero-adapter-axum", "edgezero-core", "error-stack", @@ -3567,6 +3568,7 @@ name = "trusted-server-adapter-cloudflare" version = "0.1.0" dependencies = [ "async-trait", + "base64", "bytes", "edgezero-adapter-cloudflare", "edgezero-core", diff --git a/crates/integration-tests/Cargo.lock b/crates/integration-tests/Cargo.lock index 090cf100..70dc9788 100644 --- a/crates/integration-tests/Cargo.lock +++ b/crates/integration-tests/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "ahash" version = "0.8.12" @@ -24,6 +40,27 @@ 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" @@ -89,6 +126,12 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "astral-tokio-tar" version = "0.5.6" @@ -105,6 +148,18 @@ dependencies = [ "xattr", ] +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-io", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -150,29 +205,59 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", + "hyper", + "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper", - "tower", + "tokio", + "tower 0.5.3", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -191,8 +276,15 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" @@ -205,11 +297,38 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" +dependencies = [ + "no_std_io2", +] + +[[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 = "bollard" @@ -288,6 +407,33 @@ dependencies = [ "serde_with", ] +[[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 = "build-print" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e6738dfb11354886f890621b4a34c0b177f75538023f7100b608ab9adbd66b" + [[package]] name = "bumpalo" version = "3.20.2" @@ -313,6 +459,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -322,6 +470,36 @@ 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.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.44" @@ -329,17 +507,131 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "config" +version = "0.15.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316c6237b2d38be61949ecd15268a4c6ca32570079394a2444d9ce2c72a72d8" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -349,6 +641,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -375,6 +677,53 @@ 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 = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + [[package]] name = "cssparser" version = "0.34.0" @@ -384,7 +733,20 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.11.3", + "smallvec", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", "smallvec", ] @@ -398,14 +760,65 @@ dependencies = [ "syn", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[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]] @@ -421,17 +834,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -468,7 +902,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version", @@ -477,48 +911,183 @@ dependencies = [ ] [[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[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 = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "docker_credential" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "edgezero-adapter-axum" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero?rev=38198f9839b70aef03ab971ae5876982773fc2a1#38198f9839b70aef03ab971ae5876982773fc2a1" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bytes", + "edgezero-core", + "futures", + "futures-util", + "http", + "log", + "redb", + "reqwest 0.13.3", + "simple_logger", + "thiserror", + "tokio", + "tower 0.5.3", + "tracing", +] + +[[package]] +name = "edgezero-adapter-cloudflare" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero?rev=38198f9839b70aef03ab971ae5876982773fc2a1#38198f9839b70aef03ab971ae5876982773fc2a1" dependencies = [ - "proc-macro2", - "quote", - "syn", + "anyhow", + "async-trait", + "brotli", + "bytes", + "edgezero-core", + "flate2", + "futures", + "futures-util", + "log", + "serde_json", + "worker", ] [[package]] -name = "docker_credential" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" +name = "edgezero-core" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero?rev=38198f9839b70aef03ab971ae5876982773fc2a1#38198f9839b70aef03ab971ae5876982773fc2a1" dependencies = [ - "base64 0.21.7", + "anyhow", + "async-compression", + "async-stream", + "async-trait", + "bytes", + "edgezero-macros", + "futures", + "futures-util", + "http", + "http-body", + "log", + "matchit 0.9.2", "serde", "serde_json", + "serde_urlencoded", + "thiserror", + "toml", + "tower-service", + "tracing", + "validator", + "web-time", ] [[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +name = "edgezero-macros" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero?rev=38198f9839b70aef03ab971ae5876982773fc2a1#38198f9839b70aef03ab971ae5876982773fc2a1" dependencies = [ - "dtoa", + "log", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "validator", ] -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "ego-tree" version = "0.9.0" @@ -531,6 +1100,24 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -569,6 +1156,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -606,6 +1204,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.27" @@ -623,6 +1237,16 @@ 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" @@ -635,6 +1259,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -659,6 +1289,12 @@ 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 = "futf" version = "0.1.5" @@ -766,6 +1402,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + [[package]] name = "getopts" version = "0.2.24" @@ -782,8 +1429,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -793,9 +1442,11 @@ 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]] @@ -811,6 +1462,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.13" @@ -836,13 +1498,19 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -850,6 +1518,29 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] [[package]] name = "heck" @@ -863,6 +1554,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.12" @@ -981,6 +1681,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1052,6 +1753,38 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iab_gpp" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3be2d0191a3376e0176bb3df53b2754c644ead6edd50d9494ee8fa376a70e02" +dependencies = [ + "bitstream-io", + "fnv", + "iab_gpp_derive", + "num-derive", + "num-iter", + "num-traits", + "prettyplease", + "proc-macro2", + "quote", + "strum_macros", + "syn", + "thiserror", + "walkdir", +] + +[[package]] +name = "iab_gpp_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5acda598b043c6386d20fffe86c600b63c7ca4980ee9a28f7e9aaa15d749747" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1213,19 +1946,38 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "integration-tests" version = "0.1.0" dependencies = [ + "axum", + "bytes", "derive_more 2.1.1", + "edgezero-adapter-axum", + "edgezero-core", "env_logger", "error-stack", + "http", + "http-body-util", "libc", "log", - "reqwest", + "reqwest 0.12.28", "scraper", "serde_json", "testcontainers", + "tokio", + "tower 0.4.13", + "trusted-server-adapter-axum", + "trusted-server-adapter-cloudflare", ] [[package]] @@ -1269,24 +2021,119 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "jiff" version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[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 = "jose-b64" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" +dependencies = [ + "base64ct", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "jose-jwa" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", + "serde", ] [[package]] -name = "jiff-static" -version = "0.2.23" +name = "jose-jwk" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" dependencies = [ - "proc-macro2", - "quote", - "syn", + "jose-b64", + "jose-jwa", + "p256", + "p384", + "rsa", + "serde", + "zeroize", ] [[package]] @@ -1299,6 +2146,26 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -1311,6 +2178,12 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.14" @@ -1350,6 +2223,31 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lol_html" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00aad58f6ec3990e795943872f13651e7a5fa59dca2c8f31a74faf8a0e0fb652" +dependencies = [ + "bitflags", + "cfg-if", + "cssparser 0.36.0", + "encoding_rs", + "foldhash 0.2.0", + "hashbrown 0.17.1", + "memchr", + "mime", + "precomputed-hash", + "selectors 0.37.0", + "thiserror", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -1363,8 +2261,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" dependencies = [ "log", - "phf", - "phf_codegen", + "phf 0.11.3", + "phf_codegen 0.11.3", "string_cache", "string_cache_codegen", "tendril", @@ -1381,12 +2279,24 @@ dependencies = [ "syn", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "matchit" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" + [[package]] name = "memchr" version = "2.8.0" @@ -1399,11 +2309,21 @@ 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" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -1433,6 +2353,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no_std_io2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418abd1b6d34fbf6cae440dc874771b0525a604428704c76e48b29a5e67b8003" +dependencies = [ + "memchr", +] + [[package]] name = "num" version = "0.4.3" @@ -1457,6 +2386,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -1472,6 +2417,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.46" @@ -1510,6 +2466,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", ] [[package]] @@ -1524,6 +2490,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.76" @@ -1568,6 +2540,36 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "elliptic-curve", + "primeorder", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -1616,20 +2618,80 @@ dependencies = [ "syn", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros", - "phf_shared", + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", ] [[package]] @@ -1638,8 +2700,18 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", ] [[package]] @@ -1648,18 +2720,41 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared", + "phf_shared 0.11.3", "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn", @@ -1674,6 +2769,15 @@ dependencies = [ "siphasher", ] +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -1706,6 +2810,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1718,6 +2843,17 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1773,6 +2909,37 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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" @@ -1793,25 +2960,81 @@ dependencies = [ ] [[package]] -name = "prost-derive" -version = "0.14.3" +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[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", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", ] [[package]] -name = "prost-types" -version = "0.14.3" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "prost", + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", ] [[package]] @@ -1841,6 +3064,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -1850,10 +3075,20 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", + "rand_chacha 0.9.0", "rand_core 0.9.5", ] +[[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" @@ -1869,6 +3104,9 @@ 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" @@ -1879,6 +3117,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "redb" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba239c1c1693315d3cc0e601db3b3965543afbf48c41730fdca2f069f510f4a" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1972,6 +3219,8 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", "serde", "serde_json", @@ -1979,7 +3228,44 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tower", + "tokio-rustls", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.3", "tower-http", "tower-service", "url", @@ -2002,6 +3288,50 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2036,6 +3366,7 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -2072,15 +3403,44 @@ 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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "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", @@ -2098,6 +3458,15 @@ 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.29" @@ -2144,15 +3513,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0e749d29b2064585327af5038a5a8eb73aeebad4a3472e83531a436563f7208" dependencies = [ "ahash", - "cssparser", + "cssparser 0.34.0", "ego-tree", "getopts", "html5ever", "precomputed-hash", - "selectors", + "selectors 0.26.0", "tendril", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2183,14 +3565,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" dependencies = [ "bitflags", - "cssparser", + "cssparser 0.34.0", "derive_more 0.99.20", "fxhash", "log", "new_debug_unreachable", - "phf", - "phf_codegen", + "phf 0.11.3", + "phf_codegen 0.11.3", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cfaaa6035167f0e604e42723c7650d59ee269ef220d7bbe0565602c8a0173b9" +dependencies = [ + "bitflags", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", "precomputed-hash", + "rustc-hash", "servo_arc", "smallvec", ] @@ -2211,6 +3612,29 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -2244,6 +3668,17 @@ dependencies = [ "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_repr" version = "0.1.20" @@ -2255,6 +3690,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2292,7 +3736,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn", @@ -2307,12 +3751,77 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple_logger" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7038d0e96661bf9ce647e1a6f6ef6d6f3663f66d9bf741abf14ba4876071c17" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.61.2", +] + [[package]] name = "siphasher" version = "1.0.2" @@ -2341,6 +3850,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2355,7 +3880,7 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.11.3", "precomputed-hash", "serde", ] @@ -2366,8 +3891,8 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.3", + "phf_shared 0.11.3", "proc-macro2", "quote", ] @@ -2401,6 +3926,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2540,7 +4077,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2559,8 +4098,17 @@ version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ - "num-conv", - "time-core", + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", ] [[package]] @@ -2573,16 +4121,32 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", @@ -2590,9 +4154,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -2643,6 +4207,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tonic" version = "0.14.5" @@ -2666,7 +4269,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-stream", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", "tracing", @@ -2683,6 +4286,21 @@ dependencies = [ "tonic", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.3" @@ -2715,7 +4333,7 @@ dependencies = [ "http-body", "iri-string", "pin-project-lite", - "tower", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -2738,6 +4356,7 @@ 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", @@ -2763,12 +4382,125 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trusted-server-adapter-axum" +version = "0.1.0" +dependencies = [ + "async-trait", + "edgezero-adapter-axum", + "edgezero-core", + "error-stack", + "futures", + "log", + "reqwest 0.12.28", + "simple_logger", + "tokio", + "trusted-server-core", +] + +[[package]] +name = "trusted-server-adapter-cloudflare" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "edgezero-adapter-cloudflare", + "edgezero-core", + "error-stack", + "js-sys", + "log", + "trusted-server-core", + "trusted-server-js", + "worker", +] + +[[package]] +name = "trusted-server-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "brotli", + "bytes", + "chacha20poly1305", + "chrono", + "config", + "cookie", + "derive_more 2.1.1", + "ed25519-dalek", + "edgezero-core", + "error-stack", + "flate2", + "futures", + "getrandom 0.2.17", + "hex", + "hmac", + "http", + "iab_gpp", + "jose-jwk", + "log", + "lol_html", + "matchit 0.9.2", + "mime", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "sha2", + "subtle", + "toml", + "trusted-server-js", + "trusted-server-openrtb", + "url", + "urlencoding", + "uuid", + "validator", + "web-time", +] + +[[package]] +name = "trusted-server-js" +version = "0.1.0" +dependencies = [ + "build-print", + "hex", + "sha2", + "which", +] + +[[package]] +name = "trusted-server-openrtb" +version = "0.1.0" +dependencies = [ + "log", + "serde", + "serde_json", +] + [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "ulid" version = "1.2.1" @@ -2803,6 +4535,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2849,6 +4591,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -2867,6 +4615,47 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "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 0.20.11", + "once_cell", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2879,6 +4668,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[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" @@ -2993,6 +4792,19 @@ dependencies = [ "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" @@ -3025,6 +4837,33 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3041,6 +4880,15 @@ 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" @@ -3208,6 +5056,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -3296,6 +5153,64 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "worker" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7267f3baa986254a8dace6f6a7c6ab88aef59f00c03aaad6749e048b5faaf6f6" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "http", + "http-body", + "js-sys", + "matchit 0.7.3", + "pin-project", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "worker-macros", + "worker-sys", +] + +[[package]] +name = "worker-macros" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7410081121531ec2fa111ab17b911efc601d7b6d590c0a92b847874ebeff0030" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-macro-support", + "worker-sys", +] + +[[package]] +name = "worker-sys" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4777582bf8a04174a034cb336f3702eb0e5cb444a67fdaa4fd44454ff7e2dd95" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "writeable" version = "0.6.2" @@ -3312,6 +5227,17 @@ dependencies = [ "rustix", ] +[[package]] +name = "yaml-rust2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3381,6 +5307,9 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "serde", +] [[package]] name = "zerotrie" diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 257b7878..5a4a7313 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -9,6 +9,11 @@ name = "integration" path = "tests/integration.rs" harness = true +[[test]] +name = "parity" +path = "tests/parity.rs" +harness = true + [dev-dependencies] testcontainers = { version = "0.25", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] } @@ -19,3 +24,13 @@ error-stack = "0.6" derive_more = { version = "2.0", features = ["display"] } env_logger = "0.11" libc = "0.2" +trusted-server-adapter-axum = { path = "../trusted-server-adapter-axum" } +trusted-server-adapter-cloudflare = { path = "../trusted-server-adapter-cloudflare" } +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "38198f9839b70aef03ab971ae5876982773fc2a1", features = ["axum"] } +edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "38198f9839b70aef03ab971ae5876982773fc2a1" } +axum = "0.8.9" +tower = { version = "0.4", features = ["util"] } +tokio = { version = "=1.52.3", features = ["rt-multi-thread", "macros"] } # exact pin — keep in sync with workspace-resolved tokio version +http = "1" +http-body-util = "0.1" +bytes = "1" diff --git a/crates/integration-tests/tests/parity.rs b/crates/integration-tests/tests/parity.rs new file mode 100644 index 00000000..6bcf8d49 --- /dev/null +++ b/crates/integration-tests/tests/parity.rs @@ -0,0 +1,335 @@ +//! Cross-adapter parity tests: Axum vs Cloudflare in-process. +//! +//! Sends identical requests to both adapters and asserts that: +//! - Response status codes match +//! - Critical headers (X-Geo-Info-Available, WWW-Authenticate on 401) match +//! +//! Fastly parity is verified via cargo test-fastly + Viceroy in CI. + +// Both adapters define `TrustedServerApp` — alias both to avoid name collision. +// axum::http re-exports from the `http` crate, so HeaderMap types are identical. +use axum::body::Body as AxumBody; +use axum::http::Request as AxumRequest; +use edgezero_adapter_axum::EdgeZeroAxumService; +use edgezero_core::app::Hooks as _; +use edgezero_core::http::request_builder; +use http::HeaderMap; +use tower::{Service as _, ServiceExt as _}; +use trusted_server_adapter_axum::app::TrustedServerApp as AxumApp; +use trusted_server_adapter_cloudflare::app::TrustedServerApp as CloudflareApp; + +/// Send a GET request to the Axum adapter and return (status, headers). +async fn axum_get(uri: &str) -> (u16, HeaderMap) { + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("GET") + .uri(uri) + .body(AxumBody::empty()) + .expect("should build GET request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + (resp.status().as_u16(), resp.headers().clone()) +} + +/// Send a POST request to the Axum adapter and return (status, headers, body bytes). +async fn axum_post(uri: &str, body: &str) -> (u16, HeaderMap, bytes::Bytes) { + use http_body_util::BodyExt as _; + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("POST") + .uri(uri) + .header("content-type", "application/json") + .body(AxumBody::from(body.to_owned())) + .expect("should build POST request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + let status = resp.status().as_u16(); + let headers = resp.headers().clone(); + let body_bytes = resp + .into_body() + .collect() + .await + .expect("should collect body") + .to_bytes(); + (status, headers, body_bytes) +} + +/// Convenience wrapper for tests that don't need body. +async fn axum_post_headers(uri: &str, body: &str) -> (u16, HeaderMap) { + let (s, h, _) = axum_post(uri, body).await; + (s, h) +} + +/// Send a GET request to the Cloudflare adapter and return (status, headers). +async fn cf_get(uri: &str) -> (u16, HeaderMap) { + let router = CloudflareApp::routes(); + let req = request_builder() + .method("GET") + .uri(uri) + .body(edgezero_core::body::Body::empty()) + .expect("should build GET request"); + let resp = router.oneshot(req).await.expect("should respond"); + (resp.status().as_u16(), resp.headers().clone()) +} + +/// Send a POST request to the Cloudflare adapter and return (status, headers, body bytes). +async fn cf_post(uri: &str, body: &str) -> (u16, HeaderMap, bytes::Bytes) { + let router = CloudflareApp::routes(); + let req = request_builder() + .method("POST") + .uri(uri) + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(body.to_owned())) + .expect("should build POST request"); + let resp = router.oneshot(req).await.expect("should respond"); + let status = resp.status().as_u16(); + let headers = resp.headers().clone(); + let body_bytes = resp.into_body().into_bytes(); + (status, headers, body_bytes) +} + +/// Convenience wrapper for tests that don't need body. +async fn cf_post_headers(uri: &str, body: &str) -> (u16, HeaderMap) { + let (s, h, _) = cf_post(uri, body).await; + (s, h) +} + +// --------------------------------------------------------------------------- +// Route parity: same route → same status on both adapters +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_route_status_parity() { + let (axum_status, _) = axum_get("/.well-known/trusted-server.json").await; + let (cf_status, _) = cf_get("/.well-known/trusted-server.json").await; + assert_eq!( + axum_status, cf_status, + "/.well-known/trusted-server.json must return same status: axum={axum_status} cf={cf_status}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_route_body_is_json_parity() { + // known divergence: without real signing-key configuration both adapters may + // return an error body. Assert that whichever body type each returns (JSON or + // not) is consistent: if the Cloudflare adapter returns valid JSON then the + // Axum adapter must also return valid JSON for the same route. + use http_body_util::BodyExt as _; + use serde_json::Value; + + let (axum_status, axum_body_bytes) = { + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(AxumBody::empty()) + .expect("should build GET request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + let status = resp.status().as_u16(); + let body = resp + .into_body() + .collect() + .await + .expect("should collect body") + .to_bytes(); + (status, body) + }; + + let (cf_status, cf_body_bytes) = { + let router = CloudflareApp::routes(); + let req = request_builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(edgezero_core::body::Body::empty()) + .expect("should build GET request"); + let resp = router.oneshot(req).await.expect("should respond"); + let status = resp.status().as_u16(); + let body = resp.into_body().into_bytes(); + (status, body) + }; + + // Both adapters must agree on whether the response is JSON. + let axum_json: Option = serde_json::from_slice(&axum_body_bytes).ok(); + let cf_json: Option = serde_json::from_slice(&cf_body_bytes).ok(); + assert_eq!( + axum_json.is_some(), + cf_json.is_some(), + "/.well-known/trusted-server.json body JSON-parsability must match across adapters \ + (axum_status={axum_status} cf_status={cf_status})" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn verify_signature_route_parity() { + // known divergence: without real signing-key configuration the handler may + // return 5xx. The parity assertion is that both adapters agree on the status + // (routing and middleware are wired identically). + let (axum_status, _) = axum_post_headers("/verify-signature", "{}").await; + let (cf_status, _) = cf_post_headers("/verify-signature", "{}").await; + + assert_ne!(axum_status, 404, "Axum /verify-signature must be routed"); + assert_ne!( + cf_status, 404, + "Cloudflare /verify-signature must be routed" + ); + assert_eq!( + axum_status, cf_status, + "/verify-signature must return same status: axum={axum_status} cf={cf_status}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_unauthenticated_parity() { + // Both adapters must return 401 for unauthenticated admin requests. + // The authenticated-path divergence (Axum→501 no-KV, CF→4xx no-KV) + // is separate and not covered here. + let (axum_status, axum_headers) = axum_post_headers("/admin/keys/rotate", "{}").await; + let (cf_status, cf_headers) = cf_post_headers("/admin/keys/rotate", "{}").await; + + assert_eq!( + axum_status, 401, + "Axum must return 401 for unauthenticated admin route" + ); + assert_eq!( + cf_status, 401, + "Cloudflare must return 401 for unauthenticated admin route" + ); + assert_eq!( + axum_status, cf_status, + "both adapters must return the same status for unauthenticated admin route" + ); + + assert!( + axum_headers.contains_key("www-authenticate"), + "Axum 401 must include WWW-Authenticate header" + ); + let cf_www_auth = cf_headers + .get("www-authenticate") + .expect("should have www-authenticate header on 401") + .to_str() + .expect("should be valid UTF-8"); + assert!( + cf_www_auth.starts_with("Basic realm="), + "Cloudflare 401 WWW-Authenticate must be Basic scheme: {cf_www_auth:?}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_deactivate_unauthenticated_parity() { + // Mirror of admin_rotate_unauthenticated_parity for the deactivate endpoint. + let (axum_status, axum_headers) = axum_post_headers("/admin/keys/deactivate", "{}").await; + let (cf_status, cf_headers) = cf_post_headers("/admin/keys/deactivate", "{}").await; + + assert_eq!( + axum_status, 401, + "Axum must return 401 for unauthenticated admin/keys/deactivate" + ); + assert_eq!( + cf_status, 401, + "Cloudflare must return 401 for unauthenticated admin/keys/deactivate" + ); + assert_eq!( + axum_status, cf_status, + "both adapters must return the same status for unauthenticated admin/keys/deactivate" + ); + + assert!( + axum_headers.contains_key("www-authenticate"), + "Axum 401 on admin/keys/deactivate must include WWW-Authenticate header" + ); + assert!( + cf_headers.contains_key("www-authenticate"), + "Cloudflare 401 on admin/keys/deactivate must include WWW-Authenticate header" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn geo_header_parity_on_all_responses() { + let routes_to_check: &[(&str, &str, &str)] = &[ + ("GET", "/.well-known/trusted-server.json", ""), + ("POST", "/auction", r#"{"adUnits":[]}"#), + ("POST", "/verify-signature", "{}"), + ]; + + for (method, path, body) in routes_to_check { + let (axum_status, axum_headers) = if *method == "GET" { + axum_get(path).await + } else { + axum_post_headers(path, body).await + }; + let (cf_status, cf_headers) = if *method == "GET" { + cf_get(path).await + } else { + cf_post_headers(path, body).await + }; + + assert!( + axum_headers.contains_key("x-geo-info-available"), + "Axum: {method} {path} (status={axum_status}) must have X-Geo-Info-Available" + ); + assert!( + cf_headers.contains_key("x-geo-info-available"), + "Cloudflare: {method} {path} (status={cf_status}) must have X-Geo-Info-Available" + ); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_not_challenged_by_auth_parity() { + let (axum_status, _) = axum_post_headers("/auction", r#"{"adUnits":[]}"#).await; + let (cf_status, _) = cf_post_headers("/auction", r#"{"adUnits":[]}"#).await; + + assert_ne!(axum_status, 401, "Axum /auction must not 401"); + assert_ne!(cf_status, 401, "Cloudflare /auction must not 401"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn publisher_proxy_fallback_parity() { + // Cookie (Set-Cookie) parity for the publisher proxy requires a live origin. + // Without an origin, both adapters return an error (4xx or 5xx). The parity + // assertion is that Set-Cookie presence matches across adapters regardless of + // whether the proxy succeeds. + let (axum_status, axum_headers) = axum_get("/").await; + let (cf_status, cf_headers) = cf_get("/").await; + + // Both adapters must agree: either both proxy to the origin or both fail. + assert_eq!( + axum_status >= 500, + cf_status >= 500, + "publisher fallback 5xx behaviour must match: axum={axum_status} cf={cf_status}" + ); + + let axum_has_cookie = axum_headers.contains_key("set-cookie"); + let cf_has_cookie = cf_headers.contains_key("set-cookie"); + assert_eq!( + axum_has_cookie, cf_has_cookie, + "Set-Cookie presence must match: axum={axum_has_cookie} cf={cf_has_cookie}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn unknown_route_returns_same_status_parity() { + let (axum_status, _) = axum_get("/this-route-does-not-exist-abc123").await; + let (cf_status, _) = cf_get("/this-route-does-not-exist-abc123").await; + + assert_eq!( + axum_status, cf_status, + "unknown routes must return same status: axum={axum_status} cf={cf_status}" + ); +} diff --git a/crates/trusted-server-adapter-axum/Cargo.toml b/crates/trusted-server-adapter-axum/Cargo.toml index 163530fb..79a26c54 100644 --- a/crates/trusted-server-adapter-axum/Cargo.toml +++ b/crates/trusted-server-adapter-axum/Cargo.toml @@ -29,6 +29,7 @@ trusted-server-core = { path = "../trusted-server-core" } [dev-dependencies] axum = "0.8" +base64 = { workspace = true } temp-env = { workspace = true } edgezero-adapter-axum = { workspace = true, features = ["axum"] } edgezero-core = { workspace = true } diff --git a/crates/trusted-server-adapter-axum/tests/routes.rs b/crates/trusted-server-adapter-axum/tests/routes.rs index 6d3275ea..736b3d7c 100644 --- a/crates/trusted-server-adapter-axum/tests/routes.rs +++ b/crates/trusted-server-adapter-axum/tests/routes.rs @@ -215,9 +215,140 @@ async fn finalize_middleware_sets_geo_unavailable_header() { } // --------------------------------------------------------------------------- -// Basic-auth gate test +// Basic-auth parity tests // --------------------------------------------------------------------------- +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must return 401 without credentials" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_includes_www_authenticate_header() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "should be 401 before checking header" + ); + assert!( + resp.headers().contains_key("www-authenticate"), + "401 response must include WWW-Authenticate header" + ); + let www_auth = resp + .headers() + .get("www-authenticate") + .expect("should have www-authenticate header") + .to_str() + .expect("should be valid UTF-8"); + assert!( + www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme, got: {www_auth}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_with_wrong_credentials_returns_401() { + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:wrong-password"); + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must reject wrong credentials with 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_endpoint_does_not_require_auth() { + let mut svc = make_service(); + let req = Request::builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(AxumBody::empty()) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 401, + "/.well-known/trusted-server.json must not require auth" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_endpoint_does_not_require_auth() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 401, + "/auction must not apply admin basic-auth gate" + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn admin_route_returns_non_404_non_5xx() { let mut svc = make_service(); @@ -244,3 +375,171 @@ async fn admin_route_returns_non_404_non_5xx() { "admin route should not return 5xx: got {status}" ); } + +// --------------------------------------------------------------------------- +// Admin key route full path coverage +// --------------------------------------------------------------------------- + +// Exercises the auth-fail path with a realistic key body (complements the +// generic `admin_route_without_credentials_returns_401` above). +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_auth_fail_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/rotate without credentials must return 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_deactivate_key_auth_fail_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/deactivate without credentials must return 401" + ); +} + +// --------------------------------------------------------------------------- +// First-party route smoke tests +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_proxy_is_routed() { + let mut svc = make_service(); + let req = Request::builder() + .method("GET") + .uri("/first-party/proxy") + .body(AxumBody::empty()) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/proxy must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_click_is_routed() { + let mut svc = make_service(); + let req = Request::builder() + .method("GET") + .uri("/first-party/click") + .body(AxumBody::empty()) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/click must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_sign_get_is_routed() { + let mut svc = make_service(); + let req = Request::builder() + .method("GET") + .uri("/first-party/sign") + .body(AxumBody::empty()) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 404, + "GET /first-party/sign must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_sign_post_is_routed() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/first-party/sign") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 404, + "POST /first-party/sign must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_proxy_rebuild_is_routed() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/first-party/proxy-rebuild") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/proxy-rebuild must be routed" + ); +} diff --git a/crates/trusted-server-adapter-cloudflare/Cargo.toml b/crates/trusted-server-adapter-cloudflare/Cargo.toml index 0cc3a0c4..67848f26 100644 --- a/crates/trusted-server-adapter-cloudflare/Cargo.toml +++ b/crates/trusted-server-adapter-cloudflare/Cargo.toml @@ -34,5 +34,6 @@ js-sys = { workspace = true } worker = { version = "0.7", default-features = false, features = ["http"] } [dev-dependencies] +base64 = { workspace = true } edgezero-core = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/crates/trusted-server-adapter-cloudflare/tests/routes.rs b/crates/trusted-server-adapter-cloudflare/tests/routes.rs index 831e6bd4..c6cfdb25 100644 --- a/crates/trusted-server-adapter-cloudflare/tests/routes.rs +++ b/crates/trusted-server-adapter-cloudflare/tests/routes.rs @@ -20,7 +20,7 @@ fn routes_build_without_panic() { // AuthMiddleware are wired so they cannot be removed silently. // --------------------------------------------------------------------------- -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn finalize_middleware_injects_geo_header() { // The X-Geo-Info-Available header is injected by FinalizeResponseMiddleware. // Its absence on any response means the middleware was not wired. @@ -40,7 +40,7 @@ async fn finalize_middleware_injects_geo_header() { ); } -#[tokio::test] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auth_middleware_runs_in_chain_for_protected_routes() { // Verifies that AuthMiddleware is wired into the middleware chain for auction // requests. Without it, FinalizeResponseMiddleware would still run but auth @@ -72,3 +72,315 @@ async fn auth_middleware_runs_in_chain_for_protected_routes() { "auction endpoint must be routed" ); } + +// --------------------------------------------------------------------------- +// Route smoke tests — verify all adapter routes are registered and do not 5xx +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tsjs_route_is_routed_not_5xx() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/static/tsjs=0000000000000000") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + // The tsjs route is matched by the /{*rest} catch-all. The handler returns 404 + // for an unknown hash — that is correct application behaviour, not a routing miss. + assert!(status < 500, "tsjs route must not 5xx: got {status}"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn verify_signature_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/verify-signature") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "/verify-signature must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "/admin/keys/rotate must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_deactivate_key_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "/admin/keys/deactivate must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!(resp.status().as_u16(), 404, "/auction must be routed"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_proxy_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/proxy") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + // Handlers require valid outbound proxy settings; they may return 4xx/5xx in CI. + // The assertion is routing only: the path must not fall through to the 404 not-found handler. + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/proxy must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_click_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/click") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/click must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_sign_get_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/sign") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "GET /first-party/sign must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_sign_post_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/first-party/sign") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "POST /first-party/sign must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn first_party_proxy_rebuild_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/first-party/proxy-rebuild") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 404, + "/first-party/proxy-rebuild must be routed" + ); +} + +// --------------------------------------------------------------------------- +// Basic-auth parity tests +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_returns_401() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must return 401 without credentials" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_includes_www_authenticate_header() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "should be 401 before checking header" + ); + assert!( + resp.headers().contains_key("www-authenticate"), + "401 response must include WWW-Authenticate header" + ); + let www_auth = resp + .headers() + .get("www-authenticate") + .expect("should have www-authenticate header") + .to_str() + .expect("should be valid UTF-8"); + assert!( + www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme, got: {www_auth}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_with_wrong_credentials_returns_401() { + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:wrong-password"); + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must reject wrong credentials with 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_endpoint_does_not_require_auth() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 401, + "/.well-known/trusted-server.json must not require auth" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_endpoint_does_not_require_auth() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 401, + "/auction must not apply admin basic-auth gate" + ); +} + +// --------------------------------------------------------------------------- +// Admin key route full path coverage +// --------------------------------------------------------------------------- + +// Exercises the auth-fail path with a realistic key body (complements the +// generic `admin_route_without_credentials_returns_401` above). +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_auth_fail_returns_401() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/rotate without credentials must return 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_deactivate_key_auth_fail_returns_401() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/deactivate without credentials must return 401" + ); +} diff --git a/crates/trusted-server-core/Cargo.toml b/crates/trusted-server-core/Cargo.toml index c4ea9088..b558268a 100644 --- a/crates/trusted-server-core/Cargo.toml +++ b/crates/trusted-server-core/Cargo.toml @@ -82,3 +82,7 @@ tokio = { workspace = true } [[bench]] name = "consent_decode" harness = false + +[[bench]] +name = "html_processor_bench" +harness = false diff --git a/crates/trusted-server-core/benches/html_processor_bench.rs b/crates/trusted-server-core/benches/html_processor_bench.rs new file mode 100644 index 00000000..300b8606 --- /dev/null +++ b/crates/trusted-server-core/benches/html_processor_bench.rs @@ -0,0 +1,63 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use trusted_server_core::html_processor::{create_html_processor, HtmlProcessorConfig}; +use trusted_server_core::integrations::IntegrationRegistry; +use trusted_server_core::streaming_processor::StreamProcessor as _; + +fn make_config() -> HtmlProcessorConfig { + HtmlProcessorConfig { + origin_host: "origin.bench.com".to_string(), + request_host: "proxy.bench.com".to_string(), + request_scheme: "https".to_string(), + integrations: IntegrationRegistry::default(), + } +} + +fn make_html(size_kb: usize) -> Vec { + let link_block = r#"Link + +
Ad
+"#; + + let body_content = link_block.repeat((size_kb * 1024) / link_block.len() + 1); + + format!( + r#" + + + +Benchmark Page + + +{body_content} + +"# + ) + .into_bytes() +} + +fn bench_html_processor(c: &mut Criterion) { + let mut group = c.benchmark_group("html_processor"); + + for size_kb in [10usize, 100] { + let html = make_html(size_kb); + + group.bench_with_input( + BenchmarkId::new("process_chunk", format!("{size_kb}kb")), + &html, + |b, html| { + b.iter(|| { + let config = make_config(); + let mut processor = create_html_processor(config); + processor + .process_chunk(black_box(html.as_slice()), true) + .expect("should process HTML") + }); + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, bench_html_processor); +criterion_main!(benches); diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index 6f56e85a..bdc8a603 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -46,6 +46,10 @@ fn main() { // Only write when content changes to avoid unnecessary recompilation. let dest_path = Path::new(TRUSTED_SERVER_OUTPUT_CONFIG_PATH); + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent) + .unwrap_or_else(|_| panic!("Failed to create directory for {dest_path:?}")); + } let current = fs::read_to_string(dest_path).unwrap_or_default(); if current != merged_toml { fs::write(dest_path, merged_toml) diff --git a/crates/trusted-server-core/src/html_processor.rs b/crates/trusted-server-core/src/html_processor.rs index a201a59d..cee1f6c1 100644 --- a/crates/trusted-server-core/src/html_processor.rs +++ b/crates/trusted-server-core/src/html_processor.rs @@ -534,6 +534,9 @@ mod tests { use std::io::Cursor; use std::sync::Arc; + // 2× accounts for the injected tsjs script tag plus URL attribute rewrites. + const MAX_GROWTH_FACTOR: f64 = 2.0; + fn create_test_config() -> HtmlProcessorConfig { HtmlProcessorConfig { origin_host: "origin.example.com".to_string(), @@ -1185,4 +1188,117 @@ mod tests { "should contain post-processor mutation" ); } + + #[test] + fn golden_script_tag_injected_at_head_start() { + // The trusted-server script tag must be the FIRST child of . + // Any drift in injection position breaks the page initialization order. + let html = r#" + +Test +

Hello

+"#; + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + let head_pos = output_str.find("").expect("should contain "); + let script_pos = output_str + .find(" head_pos, + "script tag must appear after opening: head_pos={head_pos}, script_pos={script_pos}" + ); + + // No other elements between and the script tag + let between = &output_str[head_pos + "".len()..script_pos]; + let trimmed = between.trim(); + assert!( + trimmed.is_empty(), + "script tag must be first child of , found content before it: {trimmed:?}" + ); + } + + #[test] + fn golden_url_rewriting_replaces_origin_in_href() { + // href attributes pointing at origin domain must be rewritten to proxy host. + let origin = "https://origin.test-publisher.com"; + let html = format!( + r#" + Link + + "# + ); + + let request_host = "proxy.test-publisher.com"; + let config = HtmlProcessorConfig { + origin_host: "origin.test-publisher.com".to_string(), + request_host: request_host.to_string(), + request_scheme: "https".to_string(), + integrations: IntegrationRegistry::default(), + }; + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + assert!( + !output_str.contains("origin.test-publisher.com"), + "origin host must not appear in rewritten HTML" + ); + assert!( + output_str.contains(request_host), + "proxy host must appear in rewritten HTML" + ); + } + + #[test] + fn golden_integration_script_is_not_double_injected() { + // Integration scripts from the registry must appear exactly once. + let html = r#" +

Content

"#; + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + let script_count = output_str.matches("/static/tsjs=").count(); + assert_eq!( + script_count, 1, + "script tag must appear exactly once, found {script_count} occurrences" + ); + } + + #[test] + fn response_size_does_not_grow_disproportionately() { + // Processing must not expand HTML by more than 2× (accounts for injected + // script tag + URL rewrites). Disproportionate growth indicates a bug + // (e.g., double-processing, buffer leak). + let html = include_str!("html_processor.test.html"); + let input_size = html.len(); + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + + let output_size = output.len(); + let growth_factor = output_size as f64 / input_size as f64; + + assert!( + growth_factor < MAX_GROWTH_FACTOR, + "processed HTML must not grow by more than {MAX_GROWTH_FACTOR}×: input={input_size}B output={output_size}B factor={growth_factor:.2}" + ); + } } diff --git a/crates/trusted-server-core/src/platform/http.rs b/crates/trusted-server-core/src/platform/http.rs index d8d3b052..1943abf3 100644 --- a/crates/trusted-server-core/src/platform/http.rs +++ b/crates/trusted-server-core/src/platform/http.rs @@ -247,3 +247,44 @@ pub trait PlatformHttpClient: Send + Sync { self.select(vec![pending]).await?.ready } } + +#[cfg(test)] +mod tests { + use super::*; + + // --------------------------------------------------------------------------- + // Error-correlation interim scope (before EdgeZero #213) + // --------------------------------------------------------------------------- + + #[test] + fn platform_response_default_has_no_backend_name() { + // On Axum/Cloudflare noop clients return PlatformResponse::new(response) + // with no backend_name. Core logic must not panic when backend_name is None. + let response = edgezero_core::http::response_builder() + .status(200) + .body(edgezero_core::body::Body::empty()) + .expect("should build response"); + let resp = PlatformResponse::new(response); + // PlatformResponse has a public field, not a method. + // PlatformPendingRequest has backend_name() method; PlatformResponse does not. + assert_eq!( + resp.backend_name, None, + "PlatformResponse without backend_name must have None field" + ); + } + + #[test] + fn platform_response_with_backend_name_is_some() { + // On Fastly, responses carry backend_name for error correlation. + let response = edgezero_core::http::response_builder() + .status(200) + .body(edgezero_core::body::Body::empty()) + .expect("should build response"); + let resp = PlatformResponse::new(response).with_backend_name("prebid-backend"); + assert_eq!( + resp.backend_name.as_deref(), + Some("prebid-backend"), + "with_backend_name must set backend_name field" + ); + } +} diff --git a/docs/superpowers/plans/2026-05-20-pr18-phase5-verification.md b/docs/superpowers/plans/2026-05-20-pr18-phase5-verification.md new file mode 100644 index 00000000..d7cc62a3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-20-pr18-phase5-verification.md @@ -0,0 +1,1553 @@ +# PR-18 Phase 5: Cross-Adapter Verification Suite + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement the Phase 5 verification gate suite — route parity, cross-adapter behavior, auth parity, auction error-correlation, HTML golden tests, and performance benchmarks — proving all three adapters (Fastly, Axum, Cloudflare) are behaviorally equivalent before production cutover. + +**Architecture:** Tests live across three layers: (1) in-process unit tests per adapter's own `tests/routes.rs`, (2) a new `parity` test binary in `crates/integration-tests` that drives Axum and Cloudflare adapters with identical requests and asserts matching status/headers, and (3) Criterion benchmarks for HTML processor throughput. Fastly parity is verified via the existing `cargo test-fastly` + Viceroy matrix. + +**Tech Stack:** Rust 2024, `tokio`, `tower`, `http` crate, `edgezero_core`, `edgezero_adapter_axum`, `edgezero_adapter_cloudflare`, `criterion 0.5` + +--- + +## File Map + +| Action | Path | Responsibility | +| ------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------ | +| Modify | `crates/trusted-server-adapter-cloudflare/tests/routes.rs` | Route smoke tests for all 10+ routes + basic-auth parity + admin key full path | +| Modify | `crates/trusted-server-adapter-axum/tests/routes.rs` | Basic-auth parity + admin key full path coverage | +| Modify | `crates/integration-tests/Cargo.toml` | Add `[[test]]` for parity binary + adapter deps | +| Create | `crates/integration-tests/tests/parity.rs` | Cross-adapter in-process parity (Axum vs Cloudflare) | +| Modify | `crates/trusted-server-core/src/auction/orchestrator.rs` | Auction async fan-out + error-correlation unit tests | +| Modify | `crates/trusted-server-core/src/html_processor.rs` | Golden output snapshot assertions | +| Create | `crates/trusted-server-core/benches/html_processor_bench.rs` | Criterion p95 latency + throughput benchmark | +| Modify | `crates/trusted-server-core/Cargo.toml` | Already has criterion; verify bench target exists | +| Modify | `.github/workflows/test.yml` | Add benchmark regression gate | + +--- + +## Task 1: Cloudflare Route Completeness + +Cloudflare `tests/routes.rs` has only 2 tests today (middleware regression + auth chain). All 10 routes from the Fastly `route_request` match list must be smoke-tested. + +**Files:** + +- Modify: `crates/trusted-server-adapter-cloudflare/tests/routes.rs` + +- [ ] **Step 1: Write failing tests for missing routes** + +Add after the existing 2 tests in `crates/trusted-server-adapter-cloudflare/tests/routes.rs`: + +```rust +// Routes currently missing from Cloudflare route smoke tests: +// /static/tsjs=*, /verify-signature, /admin/keys/rotate, +// /admin/keys/deactivate, /auction, /first-party/proxy, +// /first-party/click, /first-party/sign (GET+POST), /first-party/proxy-rebuild + +#[tokio::test] +async fn tsjs_route_is_routed_not_5xx() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/static/tsjs=0000000000000000") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert!(status < 500, "tsjs route must not 5xx: got {status}"); + assert_ne!(status, 404, "tsjs route must be registered"); +} + +#[tokio::test] +async fn verify_signature_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/verify-signature") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!(resp.status().as_u16(), 404, "/verify-signature must be routed"); +} + +#[tokio::test] +async fn admin_rotate_key_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!(resp.status().as_u16(), 404, "/admin/keys/rotate must be routed"); +} + +#[tokio::test] +async fn admin_deactivate_key_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!(resp.status().as_u16(), 404, "/admin/keys/deactivate must be routed"); +} + +#[tokio::test] +async fn auction_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!(resp.status().as_u16(), 404, "/auction must be routed"); +} + +#[tokio::test] +async fn first_party_proxy_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/proxy") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert_ne!(status, 404, "/first-party/proxy must be routed"); + assert!(status < 500, "/first-party/proxy must not 5xx: got {status}"); +} + +#[tokio::test] +async fn first_party_click_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/click") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert_ne!(status, 404, "/first-party/click must be routed"); + assert!(status < 500, "/first-party/click must not 5xx: got {status}"); +} + +#[tokio::test] +async fn first_party_sign_get_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/first-party/sign") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert_ne!(status, 404, "GET /first-party/sign must be routed"); + assert!(status < 500, "GET /first-party/sign must not 5xx: got {status}"); +} + +#[tokio::test] +async fn first_party_sign_post_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/first-party/sign") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert_ne!(status, 404, "POST /first-party/sign must be routed"); + assert!(status < 500, "POST /first-party/sign must not 5xx: got {status}"); +} + +#[tokio::test] +async fn first_party_proxy_rebuild_is_routed() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/first-party/proxy-rebuild") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + assert_ne!(status, 404, "/first-party/proxy-rebuild must be routed"); + assert!(status < 500, "/first-party/proxy-rebuild must not 5xx: got {status}"); +} +``` + +- [ ] **Step 2: Run tests and verify they compile + fail correctly** + +```bash +cargo test-cloudflare 2>&1 | tail -20 +``` + +Expected: all new tests compile; some may fail if routes are missing. + +- [ ] **Step 3: Fix any missing route wiring** + +If a test reports 404, check `crates/trusted-server-adapter-cloudflare/src/app.rs` route registration around line 354-364 and add the missing `.method("/path", handler)` entry. + +- [ ] **Step 4: Run tests and verify they pass** + +```bash +cargo test-cloudflare 2>&1 | tail -20 +``` + +Expected: all route tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-adapter-cloudflare/tests/routes.rs +git commit -m "Add route smoke tests for all Cloudflare adapter routes" +``` + +--- + +## Task 2: Basic-Auth Parity Tests + +Both adapters must: (a) return 401 on protected routes without credentials, (b) include `WWW-Authenticate: Basic realm="..."` header in 401 responses, (c) not challenge on unprotected routes. + +**Files:** + +- Modify: `crates/trusted-server-adapter-axum/tests/routes.rs` +- Modify: `crates/trusted-server-adapter-cloudflare/tests/routes.rs` + +- [ ] **Step 1: Write failing auth tests for Cloudflare adapter** + +Add to `crates/trusted-server-adapter-cloudflare/tests/routes.rs`: + +```rust +// --------------------------------------------------------------------------- +// Basic-auth parity tests +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn admin_route_without_credentials_returns_401() { + // Protected route (/admin/*) must challenge unauthenticated requests. + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must return 401 without credentials" + ); +} + +#[tokio::test] +async fn admin_route_without_credentials_includes_www_authenticate_header() { + // 401 response must include WWW-Authenticate so clients know auth scheme. + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "should be 401 before checking header" + ); + assert!( + resp.headers().contains_key("www-authenticate"), + "401 response must include WWW-Authenticate header" + ); + let www_auth = resp + .headers() + .get("www-authenticate") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme, got: {www_auth}" + ); +} + +#[tokio::test] +async fn admin_route_with_wrong_credentials_returns_401() { + // Wrong credentials must not grant access. + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:wrong-password"); + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must reject wrong credentials with 401" + ); +} + +#[tokio::test] +async fn discovery_endpoint_does_not_require_auth() { + // /.well-known/trusted-server.json is publicly accessible — no auth gate. + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(edgezero_core::body::Body::empty()) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 401, + "/.well-known/trusted-server.json must not require auth" + ); +} + +#[tokio::test] +async fn auction_endpoint_does_not_require_auth() { + // /auction is a consumer-facing endpoint — must not apply basic-auth gate. + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_ne!( + resp.status().as_u16(), + 401, + "/auction must not apply admin basic-auth gate" + ); +} +``` + +- [ ] **Step 2: Run Cloudflare auth tests** + +```bash +cargo test-cloudflare 2>&1 | grep -E "FAILED|PASSED|test result" +``` + +Expected: new auth tests pass. If 401 is not returned, the auth middleware may not be configured in test settings — check `AppState::build_state()` fallback behavior. + +- [ ] **Step 3: Write same auth tests for Axum adapter** + +Add to `crates/trusted-server-adapter-axum/tests/routes.rs` (same tests, adapted for Axum Service interface): + +```rust +// --------------------------------------------------------------------------- +// Basic-auth parity tests +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must return 401 without credentials" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_without_credentials_includes_www_authenticate_header() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "should be 401 before checking header" + ); + assert!( + resp.headers().contains_key("www-authenticate"), + "401 response must include WWW-Authenticate header" + ); + let www_auth = resp + .headers() + .get("www-authenticate") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + assert!( + www_auth.starts_with("Basic realm="), + "WWW-Authenticate must be Basic scheme, got: {www_auth}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_with_wrong_credentials_returns_401() { + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:wrong-password"); + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(AxumBody::from("{}")) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin route must reject wrong credentials with 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_endpoint_does_not_require_auth() { + let mut svc = make_service(); + let req = Request::builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(AxumBody::empty()) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 401, + "/.well-known/trusted-server.json must not require auth" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn auction_endpoint_does_not_require_auth() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/auction") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"adUnits":[]}"#)) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_ne!( + resp.status().as_u16(), + 401, + "/auction must not apply admin basic-auth gate" + ); +} +``` + +Note: `base64 = "0.22"` is in `[workspace.dependencies]`. Use `{ workspace = true }` for adapters. + +- [ ] **Step 4: Add `base64` dev-dependency to both adapter Cargo.toml files** + +In `crates/trusted-server-adapter-axum/Cargo.toml` `[dev-dependencies]`: + +```toml +base64 = { workspace = true } +``` + +In `crates/trusted-server-adapter-cloudflare/Cargo.toml` `[dev-dependencies]`: + +```toml +base64 = { workspace = true } +``` + +- [ ] **Step 5: Run both adapter auth tests** + +```bash +cargo test-axum 2>&1 | grep -E "FAILED|PASSED|test result" +cargo test-cloudflare 2>&1 | grep -E "FAILED|PASSED|test result" +``` + +Expected: all auth parity tests pass on both adapters. + +- [ ] **Step 6: Commit** + +```bash +git add crates/trusted-server-adapter-axum/tests/routes.rs \ + crates/trusted-server-adapter-cloudflare/tests/routes.rs \ + crates/trusted-server-adapter-axum/Cargo.toml \ + crates/trusted-server-adapter-cloudflare/Cargo.toml +git commit -m "Add basic-auth parity tests to Axum and Cloudflare adapters" +``` + +--- + +## Task 3: Admin Key Route Full Path Coverage + +Covers auth-fail, validation-fail, and storage-fail paths on both Axum and Cloudflare. Success path differs by adapter (Axum returns 501; Cloudflare returns 200 or storage error). + +**Files:** + +- Modify: `crates/trusted-server-adapter-axum/tests/routes.rs` +- Modify: `crates/trusted-server-adapter-cloudflare/tests/routes.rs` + +- [ ] **Step 1: Add admin key path tests to Cloudflare adapter** + +Add to `crates/trusted-server-adapter-cloudflare/tests/routes.rs`: + +```rust +// --------------------------------------------------------------------------- +// Admin key route full path coverage +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn admin_rotate_key_auth_fail_returns_401() { + // Auth-fail path: missing credentials → 401 (tested in basic-auth section, + // this test documents the specific admin key route behavior explicitly). + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/rotate without credentials must return 401" + ); +} + +#[tokio::test] +async fn admin_deactivate_key_auth_fail_returns_401() { + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/deactivate without credentials must return 401" + ); +} + +#[tokio::test] +async fn admin_rotate_key_validation_fail_returns_non_5xx() { + // Validation-fail path: authenticated but malformed body must not 5xx. + // CI settings may not have basic_auth configured; if auth passes through, + // an empty/malformed body should produce 400/422, not 500. + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:admin-pass"); + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(edgezero_core::body::Body::from("{}")) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + // Validation-fail path must be a 4xx client error — not 2xx (passed) or 5xx (crashed). + assert!( + (400..500).contains(&status), + "admin/keys/rotate with malformed body must return 4xx: got {status}" + ); +} + +#[tokio::test] +async fn admin_rotate_key_storage_fail_does_not_panic() { + // Storage-fail path: handler is reached, store operation returns error. + // In CI the store is either absent (error) or a noop. Either way must not + // panic (no 500 with a backtrace). + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:admin-pass"); + let router = TrustedServerApp::routes(); + let req = request_builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(edgezero_core::body::Body::from(r#"{"keyId":"test-key-id"}"#)) + .expect("should build request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + // Storage-fail path: handler reached, store returns error. + // Must produce a proper HTTP error (4xx or 5xx), NOT a routing miss (404) + // and NOT an unrecovered panic (which would surface as 500 on some runtimes). + assert_ne!(status, 404, "admin/keys/rotate must not 404 when authenticated"); + assert!( + status >= 400, + "admin/keys/rotate storage-fail must return error status: got {status}" + ); +} +``` + +- [ ] **Step 2: Add admin key path tests to Axum adapter** + +Add to `crates/trusted-server-adapter-axum/tests/routes.rs` (same logic, Axum returns 501 for authenticated requests since store writes are unsupported): + +```rust +// --------------------------------------------------------------------------- +// Admin key route full path coverage +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_auth_fail_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/rotate without credentials must return 401" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_authenticated_returns_not_5xx() { + // Axum dev server returns 501 Not Implemented for admin key writes. + // Auth runs first — if configured: 401. If auth skipped in CI: 501. + // Either way: must not 500. + use base64::Engine as _; + let creds = base64::engine::general_purpose::STANDARD.encode("admin:admin-pass"); + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/rotate") + .header("content-type", "application/json") + .header("authorization", format!("Basic {creds}")) + .body(AxumBody::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + let status = resp.status().as_u16(); + assert_ne!(status, 500, "admin/keys/rotate must not 5xx: got {status}"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_deactivate_key_auth_fail_returns_401() { + let mut svc = make_service(); + let req = Request::builder() + .method("POST") + .uri("/admin/keys/deactivate") + .header("content-type", "application/json") + .body(AxumBody::from(r#"{"keyId":"test-key"}"#)) + .expect("should build request"); + let resp = svc.ready().await.expect("should be ready").call(req).await.expect("should respond"); + assert_eq!( + resp.status().as_u16(), + 401, + "admin/keys/deactivate without credentials must return 401" + ); +} +``` + +- [ ] **Step 3: Run and verify** + +```bash +cargo test-axum 2>&1 | grep -E "admin_key|FAILED|test result" +cargo test-cloudflare 2>&1 | grep -E "admin_key|FAILED|test result" +``` + +Expected: all admin key path tests pass. + +- [ ] **Step 4: Commit** + +```bash +git add crates/trusted-server-adapter-axum/tests/routes.rs \ + crates/trusted-server-adapter-cloudflare/tests/routes.rs +git commit -m "Add admin key route full path coverage to Axum and Cloudflare adapters" +``` + +--- + +## Task 4: Cross-Adapter In-Process Parity Tests + +A dedicated test binary drives Axum and Cloudflare adapters with identical requests and asserts matching status codes and critical headers. This catches divergence that per-adapter smoke tests miss. + +**Files:** + +- Modify: `crates/integration-tests/Cargo.toml` +- Create: `crates/integration-tests/tests/parity.rs` + +- [ ] **Step 1: Add adapter dependencies and parity test binary to integration-tests** + +Note: `crates/integration-tests` is intentionally **excluded** from the workspace (see root `Cargo.toml` `exclude` list). Its `Cargo.toml` must use explicit versions or path deps for everything — `workspace = true` is not available here. + +Edit `crates/integration-tests/Cargo.toml`. Add after the existing `[[test]]` block: + +```toml +[[test]] +name = "parity" +path = "tests/parity.rs" +harness = true +``` + +Add to `[dev-dependencies]`: + +```toml +trusted-server-adapter-axum = { path = "../trusted-server-adapter-axum" } +trusted-server-adapter-cloudflare = { path = "../trusted-server-adapter-cloudflare" } +axum = "0.7" +tower = "0.5" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +http = "1" +http-body-util = "0.1" +bytes = "1" +base64 = "0.22" +# serde_json already in dev-dependencies for existing integration tests +``` + +Then add edgezero git deps **matching the rev in root Cargo.toml exactly**. Run this to extract the rev: + +```bash +grep 'rev = ' Cargo.toml | head -1 | grep -oP '(?<=rev = ").*(?=")' +``` + +Then add to `crates/integration-tests/Cargo.toml` `[dev-dependencies]` (replace `` with actual value): + +```toml +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero", rev = "", features = ["axum"] } +edgezero-core = { git = "https://github.com/stackpop/edgezero", rev = "" } +``` + +> **Why git dep not path dep:** edgezero is a git dependency in the workspace (not a local crate). Since `integration-tests` is workspace-excluded it cannot use `{ workspace = true }` — it must replicate the git dep form with the same rev to get Cargo to unify the dependency with the workspace's Cargo.lock. + +- [ ] **Step 2: Verify integration-tests compiles** + +```bash +cd crates/integration-tests && cargo check --test parity 2>&1 | head -30 +``` + +Expected: compiles without errors. Since integration-tests is workspace-excluded, run `cargo` from inside the `crates/integration-tests` directory or use `--manifest-path`. + +- [ ] **Step 3: Create parity test file** + +Create `crates/integration-tests/tests/parity.rs`: + +```rust +//! Cross-adapter parity tests: Axum vs Cloudflare in-process. +//! +//! Sends identical requests to both adapters and asserts that: +//! - Response status codes match +//! - Critical headers (X-Geo-Info-Available, WWW-Authenticate on 401) match +//! +//! Fastly parity is verified via cargo test-fastly + Viceroy in CI. + +// Both adapters define `TrustedServerApp` — alias both to avoid name collision. +// axum::http re-exports from the `http` crate, so HeaderMap types are identical. +use axum::body::Body as AxumBody; +use axum::http::Request as AxumRequest; +use edgezero_adapter_axum::EdgeZeroAxumService; +use edgezero_core::app::Hooks as _; +use edgezero_core::http::request_builder; +use http::HeaderMap; +use tower::{Service as _, ServiceExt as _}; +use trusted_server_adapter_axum::app::TrustedServerApp as AxumApp; +use trusted_server_adapter_cloudflare::app::TrustedServerApp as CloudflareApp; + +/// Send a GET request to the Axum adapter and return (status, headers). +async fn axum_get(uri: &str) -> (u16, HeaderMap) { + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("GET") + .uri(uri) + .body(AxumBody::empty()) + .expect("should build GET request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + // axum::http::HeaderMap is http::HeaderMap — same type, just re-exported + (resp.status().as_u16(), resp.headers().clone()) +} + +/// Send a POST request to the Axum adapter and return (status, headers, body bytes). +async fn axum_post(uri: &str, body: &str) -> (u16, HeaderMap, bytes::Bytes) { + use http_body_util::BodyExt as _; + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("POST") + .uri(uri) + .header("content-type", "application/json") + .body(AxumBody::from(body.to_owned())) + .expect("should build POST request"); + let resp = svc + .ready() + .await + .expect("should be ready") + .call(req) + .await + .expect("should respond"); + let status = resp.status().as_u16(); + let headers = resp.headers().clone(); + let body_bytes = resp.into_body().collect().await.expect("should collect body").to_bytes(); + (status, headers, body_bytes) +} + +/// Convenience wrapper for tests that don't need body. +async fn axum_post_headers(uri: &str, body: &str) -> (u16, HeaderMap) { + let (s, h, _) = axum_post(uri, body).await; + (s, h) +} + +/// Send a GET request to the Cloudflare adapter and return (status, headers). +async fn cf_get(uri: &str) -> (u16, HeaderMap) { + let router = CloudflareApp::routes(); + let req = request_builder() + .method("GET") + .uri(uri) + .body(edgezero_core::body::Body::empty()) + .expect("should build GET request"); + // router.oneshot() is infallible — returns Response directly, not Result + let resp = router.oneshot(req).await; + (resp.status().as_u16(), resp.headers().clone()) +} + +/// Send a POST request to the Cloudflare adapter and return (status, headers, body bytes). +async fn cf_post(uri: &str, body: &str) -> (u16, HeaderMap, bytes::Bytes) { + use http_body_util::BodyExt as _; + let router = CloudflareApp::routes(); + let req = request_builder() + .method("POST") + .uri(uri) + .header("content-type", "application/json") + .body(edgezero_core::body::Body::from(body.to_owned())) + .expect("should build POST request"); + let resp = router.oneshot(req).await; + let status = resp.status().as_u16(); + let headers = resp.headers().clone(); + let body_bytes = resp.into_body().collect().await.expect("should collect body").to_bytes(); + (status, headers, body_bytes) +} + +/// Convenience wrapper for tests that don't need body. +async fn cf_post_headers(uri: &str, body: &str) -> (u16, HeaderMap) { + let (s, h, _) = cf_post(uri, body).await; + (s, h) +} + +// --------------------------------------------------------------------------- +// Route parity: same route → same status class on both adapters +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn discovery_route_status_parity() { + let (axum_status, _) = axum_get("/.well-known/trusted-server.json").await; + let (cf_status, _) = cf_get("/.well-known/trusted-server.json").await; + assert_eq!( + axum_status, cf_status, + "/.well-known/trusted-server.json must return same status: axum={axum_status} cf={cf_status}" + ); +} + +#[tokio::test] +async fn discovery_route_body_is_json_parity() { + // Spec criterion 2 requires body parity. Discovery must return parseable JSON + // on both adapters (not just same status). + use http_body_util::BodyExt as _; + use serde_json::Value; + + let axum_body_bytes = { + let mut svc = EdgeZeroAxumService::new(AxumApp::routes()); + let req = AxumRequest::builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(AxumBody::empty()) + .expect("should build GET request"); + let resp = svc.ready().await.expect("ready").call(req).await.expect("respond"); + resp.into_body().collect().await.expect("collect body").to_bytes() + }; + + let cf_body_bytes = { + let router = CloudflareApp::routes(); + let req = request_builder() + .method("GET") + .uri("/.well-known/trusted-server.json") + .body(edgezero_core::body::Body::empty()) + .expect("should build GET request"); + let resp = router.oneshot(req).await; + resp.into_body().collect().await.expect("collect body").to_bytes() + }; + + let axum_json: Option = serde_json::from_slice(&axum_body_bytes).ok(); + let cf_json: Option = serde_json::from_slice(&cf_body_bytes).ok(); + assert!(axum_json.is_some(), "Axum discovery must return valid JSON body"); + assert!(cf_json.is_some(), "Cloudflare discovery must return valid JSON body"); +} + +#[tokio::test] +async fn verify_signature_route_parity() { + // Spec criterion 2: "signing responses" must have status parity. + // Both adapters must reach the handler (not 404) and not panic (not 500). + let (axum_status, _) = axum_post_headers("/verify-signature", "{}").await; + let (cf_status, _) = cf_post_headers("/verify-signature", "{}").await; + + assert_ne!(axum_status, 404, "Axum /verify-signature must be routed"); + assert_ne!(cf_status, 404, "Cloudflare /verify-signature must be routed"); + assert!(axum_status < 500, "Axum /verify-signature must not 5xx: {axum_status}"); + assert!(cf_status < 500, "Cloudflare /verify-signature must not 5xx: {cf_status}"); + assert_eq!( + axum_status, cf_status, + "/verify-signature must return same status: axum={axum_status} cf={cf_status}" + ); +} + +#[tokio::test] +async fn admin_rotate_unauthenticated_parity() { + let (axum_status, axum_headers) = axum_post_headers("/admin/keys/rotate", "{}").await; + let (cf_status, cf_headers) = cf_post_headers("/admin/keys/rotate", "{}").await; + + assert_eq!( + axum_status, cf_status, + "/admin/keys/rotate unauthenticated must return same status: axum={axum_status} cf={cf_status}" + ); + assert_eq!(axum_status, 401, "both adapters must return 401"); + + let axum_www_auth = axum_headers + .get("www-authenticate") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + let cf_www_auth = cf_headers + .get("www-authenticate") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + assert!( + axum_www_auth.starts_with("Basic realm="), + "Axum 401 WWW-Authenticate must be Basic scheme: {axum_www_auth:?}" + ); + assert!( + cf_www_auth.starts_with("Basic realm="), + "Cloudflare 401 WWW-Authenticate must be Basic scheme: {cf_www_auth:?}" + ); + // Values should match (same realm string) — documents intentional divergence if not + assert_eq!( + axum_www_auth, cf_www_auth, + "WWW-Authenticate values must match across adapters" + ); +} + +#[tokio::test] +async fn geo_header_parity_on_all_responses() { + // X-Geo-Info-Available must be present on every response (FinalizeResponseMiddleware). + let routes_to_check: &[(&str, &str, &str)] = &[ + ("GET", "/.well-known/trusted-server.json", ""), + ("POST", "/auction", r#"{"adUnits":[]}"#), + ("POST", "/verify-signature", "{}"), + ]; + + for (method, path, body) in routes_to_check { + let (axum_status, axum_headers) = if *method == "GET" { + axum_get(path).await + } else { + axum_post_headers(path, body).await + }; + let (cf_status, cf_headers) = if *method == "GET" { + cf_get(path).await + } else { + cf_post_headers(path, body).await + }; + + assert!( + axum_headers.contains_key("x-geo-info-available"), + "Axum: {method} {path} (status={axum_status}) must have X-Geo-Info-Available" + ); + assert!( + cf_headers.contains_key("x-geo-info-available"), + "Cloudflare: {method} {path} (status={cf_status}) must have X-Geo-Info-Available" + ); + } +} + +#[tokio::test] +async fn auction_not_challenged_by_auth_parity() { + // /auction must not be gated by admin basic-auth on either adapter. + let (axum_status, _) = axum_post_headers("/auction", r#"{"adUnits":[]}"#).await; + let (cf_status, _) = cf_post_headers("/auction", r#"{"adUnits":[]}"#).await; + + assert_ne!(axum_status, 401, "Axum /auction must not 401"); + assert_ne!(cf_status, 401, "Cloudflare /auction must not 401"); +} + +#[tokio::test] +async fn cookie_behavior_note() { + // Cookie (Set-Cookie) behavior is set by the publisher proxy handler when + // the origin serves a full HTML page. In-process tests without a live origin + // cannot exercise this path. Cookie parity is covered by the Docker-based + // integration tests in test_all_combinations (marked #[ignore] — requires Docker). + // + // What we CAN verify in-process: publisher route does NOT set a cookie when + // the origin is unavailable (no origin configured → no EC cookie attempt). + let (axum_status, axum_headers) = axum_get("/").await; + let (cf_status, cf_headers) = cf_get("/").await; + + // Neither adapter should crash on publisher fallback path + assert!(axum_status < 500, "Axum publisher fallback must not 5xx: {axum_status}"); + assert!(cf_status < 500, "Cloudflare publisher fallback must not 5xx: {cf_status}"); + + // If a Set-Cookie is set, both adapters must set it (presence parity) + let axum_has_cookie = axum_headers.contains_key("set-cookie"); + let cf_has_cookie = cf_headers.contains_key("set-cookie"); + assert_eq!( + axum_has_cookie, cf_has_cookie, + "Set-Cookie presence must match: axum={axum_has_cookie} cf={cf_has_cookie}" + ); +} + +#[tokio::test] +async fn unknown_route_returns_same_status_parity() { + // Both adapters must handle unknown routes the same way (not crash). + let (axum_status, _) = axum_get("/this-route-does-not-exist-abc123").await; + let (cf_status, _) = cf_get("/this-route-does-not-exist-abc123").await; + + assert_eq!( + axum_status, cf_status, + "unknown routes must return same status: axum={axum_status} cf={cf_status}" + ); +} +``` + +- [ ] **Step 4: Run parity tests** + +```bash +cd crates/integration-tests && cargo test --test parity 2>&1 | tail -30 +``` + +Expected: all parity tests pass. If status codes diverge, investigate the differing adapter behavior and add an exception comment documenting the known difference if intentional. + +- [ ] **Step 5: Commit** + +```bash +git add crates/integration-tests/Cargo.toml crates/integration-tests/tests/parity.rs +git commit -m "Add cross-adapter in-process parity test suite (Axum vs Cloudflare)" +``` + +--- + +## Task 5: Auction Async Fan-Out and Error-Correlation Tests + +Verify that `PlatformResponse::backend_name` is `None` on Axum/Cloudflare (as expected before EdgeZero #213), and that the auction orchestrator handles this gracefully without panicking. + +**Files:** + +- Modify: `crates/trusted-server-core/src/platform/http.rs` (where `PlatformResponse` is defined) + +- [ ] **Step 1: Locate existing test module in orchestrator.rs** + +```bash +grep -n "#\[cfg(test)\]\|mod tests\|#\[test\]" crates/trusted-server-core/src/auction/orchestrator.rs | head -20 +``` + +- [ ] **Step 2: Write error-correlation tests** + +These tests live in `crates/trusted-server-core/src/platform/http.rs` `#[cfg(test)]` module (where `PlatformResponse` is defined), not in `orchestrator.rs`. Add after the existing tests in that file's `#[cfg(test)]` module: + +```rust +// --------------------------------------------------------------------------- +// Error-correlation interim scope (before EdgeZero #213) +// --------------------------------------------------------------------------- + +#[test] +fn platform_response_default_has_no_backend_name() { + // On Axum/Cloudflare noop clients return PlatformResponse::new(response) + // with no backend_name. Core logic must not panic when backend_name is None. + let response = edgezero_core::http::Response::builder() + .status(200) + .body(edgezero_core::body::Body::empty()) + .expect("should build response"); + let resp = PlatformResponse::new(response); + // PlatformResponse has a public field, not a method. + // PlatformPendingRequest has backend_name() method; PlatformResponse does not. + assert_eq!( + resp.backend_name, + None, + "PlatformResponse without backend_name must have None field" + ); +} + +#[test] +fn platform_response_with_backend_name_is_some() { + // On Fastly, responses carry backend_name for error correlation. + let response = edgezero_core::http::Response::builder() + .status(200) + .body(edgezero_core::body::Body::empty()) + .expect("should build response"); + let resp = PlatformResponse::new(response).with_backend_name("prebid-backend"); + assert_eq!( + resp.backend_name.as_deref(), + Some("prebid-backend"), + "with_backend_name must set backend_name field" + ); +} +``` + +Confirmed: `platform/http.rs` has **no existing `#[cfg(test)]` module** (verified by grep). Must create one. Add at end of file: + +```rust +#[cfg(test)] +mod tests { + use super::*; + + // error-correlation tests go here +} +``` + +**File:** `crates/trusted-server-core/src/platform/http.rs` + +```` + +- [ ] **Step 3: Run the tests** + +```bash +cargo test -p trusted-server-core auction::orchestrator::tests 2>&1 | tail -20 +```` + +Expected: both tests pass. + +- [ ] **Step 4: Run tests** + +```bash +cargo test -p trusted-server-core platform_response 2>&1 | tail -15 +``` + +Expected: both tests pass (test names match `platform_response_*`). + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-core/src/platform/http.rs +git commit -m "Add error-correlation unit tests for PlatformResponse backend_name" +``` + +--- + +## Task 6: HTML Rewriting Golden Tests + +Strengthen `html_processor.rs` tests with precise snapshot-style assertions that will catch regressions in injection position, URL rewriting correctness, and integration rewriter behavior. + +**Files:** + +- Modify: `crates/trusted-server-core/src/html_processor.rs` + +- [ ] **Step 1: Find the existing `test_real_publisher_html` test and helper** + +```bash +grep -n "fn test_real_publisher_html\|fn create_test_config\|fn test_integration_registry" \ + crates/trusted-server-core/src/html_processor.rs +``` + +Confirmed: `create_test_config()` is at line ~537. `test_real_publisher_html` at ~728. + +`HtmlProcessorConfig` actual fields (no `script_tag`): + +```rust +pub struct HtmlProcessorConfig { + pub origin_host: String, + pub request_host: String, + pub request_scheme: String, + pub integrations: IntegrationRegistry, // NOT Arc<> wrapped +} +``` + +- [ ] **Step 2: Add golden injection position test** + +In the `#[cfg(test)]` module of `crates/trusted-server-core/src/html_processor.rs`, add after the existing tests: + +```rust +#[test] +fn golden_script_tag_injected_at_head_start() { + // The trusted-server script tag must be the FIRST child of . + // Any drift in injection position breaks the page initialization order. + let html = r#" + +Test +

Hello

+"#; + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + let head_pos = output_str + .find("") + .expect("should contain "); + let script_pos = output_str + .find(" head_pos, + "script tag must appear after opening: head_pos={head_pos}, script_pos={script_pos}" + ); + + // No other elements between and the script tag + let between = &output_str[head_pos + "".len()..script_pos]; + let trimmed = between.trim(); + assert!( + trimmed.is_empty(), + "script tag must be first child of , found content before it: {trimmed:?}" + ); +} + +#[test] +fn golden_url_rewriting_replaces_origin_in_href() { + // href attributes pointing at origin domain must be rewritten to proxy host. + let origin = "https://origin.test-publisher.com"; + let html = format!( + r#" + Link + + "# + ); + + let request_host = "proxy.test-publisher.com"; + let config = HtmlProcessorConfig { + origin_host: "origin.test-publisher.com".to_string(), + request_host: request_host.to_string(), + request_scheme: "https".to_string(), + integrations: IntegrationRegistry::default(), + }; + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + assert!( + !output_str.contains("origin.test-publisher.com"), + "origin host must not appear in rewritten HTML" + ); + assert!( + output_str.contains(request_host), + "proxy host must appear in rewritten HTML" + ); +} + +#[test] +fn golden_integration_script_is_not_double_injected() { + // Integration scripts from the registry must appear exactly once. + let html = r#" +

Content

"#; + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + let output_str = std::str::from_utf8(&output).expect("should be valid UTF-8"); + + let script_count = output_str.matches("/static/tsjs=").count(); + assert_eq!( + script_count, 1, + "script tag must appear exactly once, found {script_count} occurrences" + ); +} +``` + +- [ ] **Step 3: Run golden tests** + +```bash +cargo test -p trusted-server-core html_processor 2>&1 | tail -30 +``` + +Expected: all golden tests pass. If they fail, diagnose whether the processor behavior is wrong or the test assumptions are wrong (e.g., `create_test_config()` not available). + +- [ ] **Step 4: Fix any helper gaps** + +If `create_test_config()` doesn't exist, add it to the test module following the config pattern in `test_real_publisher_html` (around line 730). + +- [ ] **Step 5: Commit** + +```bash +git add crates/trusted-server-core/src/html_processor.rs +git commit -m "Add HTML rewriting golden regression tests" +``` + +--- + +## Task 7: Performance Benchmarks (p95 Latency + Response Size) + +Criterion benchmarks for the HTML processor establish a baseline for regression detection. Benchmark name: `html_processor_bench`. + +**Files:** + +- Modify: `crates/trusted-server-core/Cargo.toml` (verify bench target) +- Create: `crates/trusted-server-core/benches/html_processor_bench.rs` + +- [ ] **Step 1: Verify existing Cargo.toml bench configuration** + +```bash +grep -A5 "\[\[bench\]\]" crates/trusted-server-core/Cargo.toml +``` + +There is already a `consent_decode` bench. Add a second `[[bench]]` entry. + +- [ ] **Step 2: Add benchmark entry to Cargo.toml** + +In `crates/trusted-server-core/Cargo.toml`, add after the existing `[[bench]]` block: + +```toml +[[bench]] +name = "html_processor_bench" +harness = false +``` + +- [ ] **Step 3: Create benchmark file** + +Create `crates/trusted-server-core/benches/html_processor_bench.rs`: + +```rust +//! Performance benchmarks for the HTML processor. +//! +//! Baseline targets (to be updated after first run establishes actuals): +//! - process_chunk (10KB HTML): < 2ms mean +//! - process_chunk (100KB HTML): < 10ms mean +//! +//! Run with: cargo bench -p trusted-server-core --bench html_processor_bench + +use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main}; +use trusted_server_core::html_processor::{HtmlProcessorConfig, create_html_processor}; +use trusted_server_core::integrations::IntegrationRegistry; +use trusted_server_core::streaming_processor::StreamProcessor as _; + +fn make_config() -> HtmlProcessorConfig { + // HtmlProcessorConfig fields: origin_host, request_host, request_scheme, integrations + // No script_tag field — the script tag is generated from the configured tsjs module list + HtmlProcessorConfig { + origin_host: "origin.bench.com".to_string(), + request_host: "proxy.bench.com".to_string(), + request_scheme: "https".to_string(), + integrations: IntegrationRegistry::default(), + } +} + +fn make_html(size_kb: usize) -> Vec { + // Construct a realistic HTML page of approximately `size_kb` KB + // with links, images, and ad slots to exercise all rewriter paths. + let link_block = r#"Link + + +"#; + + let body_content = link_block.repeat((size_kb * 1024) / link_block.len() + 1); + + format!( + r#" + + + +Benchmark Page + + +{body_content} + +"# + ) + .into_bytes() +} + +fn bench_html_processor(c: &mut Criterion) { + let mut group = c.benchmark_group("html_processor"); + + for size_kb in [10usize, 100] { + let html = make_html(size_kb); + + group.bench_with_input( + BenchmarkId::new("process_chunk", format!("{size_kb}kb")), + &html, + |b, html| { + b.iter(|| { + let config = make_config(); + // `create_html_processor` returns `impl StreamProcessor` + // which exposes `process_chunk(&mut self, chunk: &[u8], is_last: bool)` + let mut processor = create_html_processor(config); + let result = processor + .process_chunk(html.as_slice(), true) + .expect("should process HTML"); + result + }); + }, + ); + } + + group.finish(); +} + +criterion_group!(benches, bench_html_processor); +criterion_main!(benches); +``` + +- [ ] **Step 4: Run benchmarks to establish baseline** + +```bash +cargo bench -p trusted-server-core --bench html_processor_bench 2>&1 | tail -20 +``` + +Expected: benchmarks complete. Record the mean latencies from the output for future regression comparison. + +- [ ] **Step 5: Verify response size by adding a measurement test** + +Add to the benchmark file a single measurement test (not a Criterion bench) to assert response size bounds. Alternatively, add this to the `html_processor.rs` unit tests: + +In `crates/trusted-server-core/src/html_processor.rs` `#[cfg(test)]` module: + +```rust +#[test] +fn response_size_does_not_grow_disproportionately() { + // Processing must not expand HTML by more than 2× (accounts for injected + // script tag + URL rewrites). Disproportionate growth indicates a bug + // (e.g., double-processing, buffer leak). + // File exists at crates/trusted-server-core/src/html_processor.test.html + // (already used by test_real_publisher_html at line ~728). + let html = include_str!("html_processor.test.html"); + let input_size = html.len(); + + let config = create_test_config(); + let mut processor = create_html_processor(config); + let output = processor + .process_chunk(html.as_bytes(), true) + .expect("should process HTML"); + + let output_size = output.len(); + let growth_factor = output_size as f64 / input_size as f64; + + assert!( + growth_factor < 2.0, + "processed HTML must not grow by more than 2×: input={input_size}B output={output_size}B factor={growth_factor:.2}" + ); +} +``` + +- [ ] **Step 6: Run tests** + +```bash +cargo test -p trusted-server-core response_size 2>&1 | tail -10 +``` + +Expected: passes. + +- [ ] **Step 7: Commit** + +```bash +git add crates/trusted-server-core/Cargo.toml \ + crates/trusted-server-core/benches/html_processor_bench.rs \ + crates/trusted-server-core/src/html_processor.rs +git commit -m "Add Criterion benchmarks and response size regression test for HTML processor" +``` + +--- + +## Task 8: CI Verification Gate + +Update the CI workflows to run the new parity test binary and include a benchmark smoke-run (no regression threshold yet — establishes baseline). + +**Files:** + +- Modify: `.github/workflows/test.yml` + +- [ ] **Step 1: Add parity test job to test.yml** + +Add after the `test-cloudflare` job in `.github/workflows/test.yml`: + +```yaml +test-parity: + name: cargo test (cross-adapter parity) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Retrieve Rust version + id: rust-version + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + shell: bash + + - name: Set up Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ steps.rust-version.outputs.rust-version }} + cache-shared-key: cargo-${{ runner.os }} + + - name: Run cross-adapter parity tests + run: cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity +``` + +- [ ] **Step 2: Add benchmark smoke-run to axum job (optional — compile-only check)** + +In the `test-axum` job, add after the test step: + +```yaml +- name: Run HTML processor benchmarks (smoke run) + run: cargo bench -p trusted-server-core --bench html_processor_bench -- --test + # `-- --test` runs benchmarks as tests (1 iteration), not full bench. + # Full benchmarking is done manually, not in CI. +``` + +- [ ] **Step 3: Verify workflow YAML is valid** + +```bash +# Check for YAML syntax errors +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/test.yml'))" && echo "YAML valid" +``` + +Expected: `YAML valid`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/test.yml +git commit -m "Add cross-adapter parity and benchmark CI gates for Phase 5 verification" +``` + +--- + +## Verification Checklist + +After all tasks are complete, run the full suite: + +```bash +# Fastly + core +cargo test-fastly + +# Axum adapter (includes auth parity + admin key tests) +cargo test-axum + +# Cloudflare adapter (includes route completeness + auth parity + admin key tests) +cargo test-cloudflare + +# Cross-adapter parity +cargo test --manifest-path crates/integration-tests/Cargo.toml --test parity + +# HTML golden + response size + error-correlation +cargo test -p trusted-server-core + +# Benchmarks (smoke run) +cargo bench -p trusted-server-core --bench html_processor_bench -- --test +``` + +All commands must exit 0 before marking this PR complete.