diff --git a/.cargo/config.toml b/.cargo/config.toml index e0e3f5be..47b3b070 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,8 +1,2 @@ -[alias] -test_details = ["test", "--target", "aarch64-apple-darwin"] - -[build] -target = "wasm32-wasip1" - [target.'cfg(all(target_arch = "wasm32"))'] runner = "viceroy run -C ../../fastly.toml -- " diff --git a/.github/actions/setup-integration-test-env/action.yml b/.github/actions/setup-integration-test-env/action.yml index d44858f3..fcbaee06 100644 --- a/.github/actions/setup-integration-test-env/action.yml +++ b/.github/actions/setup-integration-test-env/action.yml @@ -17,6 +17,10 @@ inputs: description: Build the trusted-server WASM binary for integration tests. required: false default: "true" + build-axum: + description: Build the trusted-server-axum native binary for integration tests. + required: false + default: "true" build-test-images: description: Build the framework Docker images used by integration tests. required: false @@ -75,6 +79,16 @@ runs: TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false" run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1 + - name: Build Axum native binary + if: ${{ inputs.build-axum == 'true' }} + shell: bash + env: + TRUSTED_SERVER__PUBLISHER__ORIGIN_URL: http://127.0.0.1:${{ inputs.origin-port }} + TRUSTED_SERVER__PUBLISHER__PROXY_SECRET: integration-test-proxy-secret + TRUSTED_SERVER__SYNTHETIC__SECRET_KEY: integration-test-secret-key + TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK: "false" + run: cargo build -p trusted-server-adapter-axum + - name: Build WordPress test container if: ${{ inputs.build-test-images == 'true' }} shell: bash diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index da467583..2c570ef3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -14,6 +14,7 @@ env: ORIGIN_PORT: 8888 ARTIFACTS_DIR: /tmp/integration-test-artifacts WASM_ARTIFACT_PATH: /tmp/integration-test-artifacts/wasm/trusted-server-adapter-fastly.wasm + AXUM_ARTIFACT_PATH: /tmp/integration-test-artifacts/axum/trusted-server-axum DOCKER_ARTIFACT_PATH: /tmp/integration-test-artifacts/docker/test-images.tar jobs: @@ -32,8 +33,9 @@ jobs: - name: Package integration test artifacts run: | - mkdir -p "$(dirname "$WASM_ARTIFACT_PATH")" "$(dirname "$DOCKER_ARTIFACT_PATH")" + mkdir -p "$(dirname "$WASM_ARTIFACT_PATH")" "$(dirname "$AXUM_ARTIFACT_PATH")" "$(dirname "$DOCKER_ARTIFACT_PATH")" cp target/wasm32-wasip1/release/trusted-server-adapter-fastly.wasm "$WASM_ARTIFACT_PATH" + cp target/debug/trusted-server-axum "$AXUM_ARTIFACT_PATH" docker save \ --output "$DOCKER_ARTIFACT_PATH" \ test-wordpress:latest test-nextjs:latest @@ -69,6 +71,9 @@ jobs: name: integration-test-artifacts path: ${{ env.ARTIFACTS_DIR }} + - name: Make binaries executable + run: chmod +x "$AXUM_ARTIFACT_PATH" + - name: Load integration test Docker images run: docker load --input "$DOCKER_ARTIFACT_PATH" @@ -80,6 +85,7 @@ jobs: -- --include-ignored --skip test_wordpress_fastly --skip test_nextjs_fastly --test-threads=1 env: WASM_BINARY_PATH: ${{ env.WASM_ARTIFACT_PATH }} + AXUM_BINARY_PATH: ${{ env.AXUM_ARTIFACT_PATH }} INTEGRATION_ORIGIN_PORT: ${{ env.ORIGIN_PORT }} RUST_LOG: info diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5eea36a7..aa86b537 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,30 @@ jobs: run: cargo install --git https://github.com/fastly/Viceroy viceroy - name: Run tests - run: cargo test --workspace + run: cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 + + test-axum: + name: cargo test (axum native) + 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: Build Axum adapter + run: cargo build -p trusted-server-adapter-axum + + - name: Run Axum adapter tests + run: cargo test -p trusted-server-adapter-axum test-typescript: name: vitest diff --git a/.gitignore b/.gitignore index af70c452..e9e08c45 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ /bin /pkg /target + +# EdgeZero local KV store (created by axum dev server) +.edgezero/ /crates/integration-tests/target # env diff --git a/CLAUDE.md b/CLAUDE.md index 32957649..e28ec32b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,6 +15,7 @@ real-time bidding integration, and publisher-side JavaScript injection. crates/ trusted-server-core/ # Core library — shared logic, integrations, HTML processing trusted-server-adapter-fastly/ # Fastly Compute entry point (wasm32-wasip1 binary) + trusted-server-adapter-axum/ # Axum dev server entry point (native binary) js/ # TypeScript/JS build — per-integration IIFE bundles lib/ # TS source, Vitest tests, esbuild pipeline ``` @@ -49,13 +50,20 @@ fastly compute serve # Deploy to Fastly fastly compute publish + +# Run Axum dev server (native — no Viceroy) +cargo run -p trusted-server-adapter-axum + +# Test Axum adapter only +cargo test -p trusted-server-adapter-axum ``` ### Testing & Quality ```bash -# Run all Rust tests (uses viceroy) -cargo test --workspace +# Run all Rust tests (Fastly/WASM crates via Viceroy, axum separately) +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 +cargo test -p trusted-server-adapter-axum # Format cargo fmt --all -- --check diff --git a/Cargo.lock b/Cargo.lock index 4ae4308f..928b1e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,12 +126,92 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit 0.8.4", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -158,9 +238,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ "serde_core", ] @@ -244,15 +324,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chacha20" version = "0.9.1" @@ -353,6 +447,34 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[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.37" @@ -436,6 +558,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -516,7 +648,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -528,7 +660,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "typenum", ] @@ -751,6 +883,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ed25519" version = "2.2.3" @@ -769,13 +907,36 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.6.4", "serde", "sha2 0.10.9", "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.2", + "simple_logger", + "thiserror 2.0.17", + "tokio", + "tower 0.5.3", + "tracing", +] + [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" @@ -813,7 +974,7 @@ dependencies = [ "http", "http-body", "log", - "matchit", + "matchit 0.9.1", "serde", "serde_json", "serde_urlencoded", @@ -857,7 +1018,7 @@ dependencies = [ "ff", "generic-array", "group", - "rand_core", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", @@ -912,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1007,7 +1168,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1060,6 +1221,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 = "futures" version = "0.3.32" @@ -1179,9 +1346,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1191,7 +1360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1310,6 +1479,91 @@ dependencies = [ "http", ] +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iab_gpp" version = "0.1.2" @@ -1493,6 +1747,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1501,7 +1771,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1528,6 +1798,60 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[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 2.0.111", +] + +[[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" @@ -1596,9 +1920,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" @@ -1650,7 +1974,7 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ff94cb6aef6ee52afd2c69331e9109906d855e82bd241f3110dfdf6185899ab" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cssparser", "encoding_rs", @@ -1663,6 +1987,18 @@ dependencies = [ "thiserror 2.0.17", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[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.1" @@ -1691,6 +2027,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1708,7 +2055,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1775,6 +2122,15 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1793,6 +2149,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -1954,6 +2316,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2073,14 +2455,70 @@ dependencies = [ ] [[package]] -name = "quote" -version = "1.0.42" +name = "quinn" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "proc-macro2", -] - + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + [[package]] name = "r-efi" version = "5.3.0" @@ -2094,8 +2532,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -2105,7 +2553,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2117,13 +2575,31 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -2155,13 +2631,100 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "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", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "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", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "once_cell", "serde", "serde_derive", @@ -2182,7 +2745,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -2220,11 +2783,87 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2248,6 +2887,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2267,13 +2915,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "feef350c36147532e1b79ea5c1f3791373e61cbd9a6a2615413b3807bb164fb7" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cssparser", "derive_more", "log", @@ -2347,6 +3018,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" @@ -2418,6 +3100,16 @@ 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" @@ -2425,7 +3117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2434,6 +3126,18 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[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.1" @@ -2452,6 +3156,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "spin" version = "0.9.8" @@ -2520,6 +3234,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2588,7 +3311,9 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -2640,6 +3365,58 @@ dependencies = [ "serde_json", ] +[[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.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "toml" version = "0.9.8" @@ -2701,6 +3478,61 @@ version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -2713,6 +3545,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", @@ -2738,6 +3571,24 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trusted-server-adapter-axum" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "edgezero-adapter-axum", + "edgezero-core", + "error-stack", + "log", + "reqwest 0.12.28", + "simple_logger", + "temp-env", + "tokio", + "tower 0.4.13", + "trusted-server-core", +] + [[package]] name = "trusted-server-adapter-fastly" version = "0.1.0" @@ -2788,10 +3639,10 @@ dependencies = [ "jose-jwk", "log", "lol_html", - "matchit", + "matchit 0.9.1", "mime", "pin-project-lite", - "rand", + "rand 0.8.5", "regex", "serde", "serde_json", @@ -2826,6 +3677,12 @@ dependencies = [ "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" @@ -2872,6 +3729,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2953,6 +3816,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2981,6 +3853,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.106" @@ -3013,6 +3898,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -3023,6 +3918,24 @@ 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.0" @@ -3040,7 +3953,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -3102,6 +4015,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -3111,6 +4051,192 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" @@ -3138,7 +4264,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 13b15135..e8d803a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,12 +3,13 @@ resolver = "2" members = [ "crates/trusted-server-core", "crates/trusted-server-adapter-fastly", + "crates/trusted-server-adapter-axum", "crates/js", "crates/openrtb", ] -# integration-tests is intentionally excluded from workspace members because it -# requires a native target (testcontainers, reqwest) while the workspace default -# is wasm32-wasip1. Run it via: ./scripts/integration-tests.sh +# Native-only crates excluded from the workspace: +# - integration-tests: Run via ./scripts/integration-tests.sh +# - openrtb-codegen: code-generation tool, native only exclude = [ "crates/integration-tests", "crates/openrtb-codegen", @@ -78,6 +79,7 @@ matchit = "0.9" mime = "0.3" pin-project-lite = "0.2" rand = "0.8" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } regex = "1.12.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.149" diff --git a/README.md b/README.md index 82dfe7b5..aab94f35 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,16 @@ See the [Getting Started guide](https://iabtechlab.github.io/trusted-server/guid # Build cargo build -# Run tests -cargo test +# Run tests (Fastly/WASM crates — requires Viceroy) +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 -# Start local server +# Run tests (Axum native adapter) +cargo test -p trusted-server-adapter-axum + +# Start local server — Axum (no Fastly CLI or Viceroy required) +cargo run -p trusted-server-adapter-axum + +# Start local server — Fastly (requires Fastly CLI + Viceroy) fastly compute serve ``` @@ -41,8 +47,9 @@ cargo fmt # Lint cargo clippy --workspace --all-targets --all-features -- -D warnings -# Run tests -cargo test +# Run all tests +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 +cargo test -p trusted-server-adapter-axum ``` See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines. diff --git a/crates/integration-tests/tests/environments/axum.rs b/crates/integration-tests/tests/environments/axum.rs new file mode 100644 index 00000000..b6bfdc4d --- /dev/null +++ b/crates/integration-tests/tests/environments/axum.rs @@ -0,0 +1,123 @@ +use crate::common::runtime::{ + RuntimeEnvironment, RuntimeProcess, RuntimeProcessHandle, TestError, TestResult, +}; +use error_stack::ResultExt as _; +use std::io::{BufRead as _, BufReader}; +use std::path::Path; +use std::process::{Child, Command, Stdio}; + +/// Default port the Axum dev server binds to when no `PORT` env var is supplied. +const AXUM_DEFAULT_PORT: u16 = 8787; + +/// Axum native dev-server runtime environment. +/// +/// Spawns the pre-built `trusted-server-axum` binary directly (no WASM, no +/// Viceroy). The binary must have been built before running integration tests: +/// +/// ```sh +/// cargo build -p trusted-server-adapter-axum +/// ``` +/// +/// The WASM binary path argument is unused — it exists only to satisfy the +/// [`RuntimeEnvironment`] trait shared with Fastly. +pub struct AxumDevServer; + +impl RuntimeEnvironment for AxumDevServer { + fn id(&self) -> &'static str { + "axum" + } + + fn spawn(&self, _wasm_path: &Path) -> TestResult { + let binary = self.binary_path(); + let port = super::find_available_port().unwrap_or(AXUM_DEFAULT_PORT); + + let mut child = Command::new(&binary) + .env("PORT", port.to_string()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .spawn() + .change_context(TestError::RuntimeSpawn) + .attach(format!( + "Failed to spawn trusted-server-axum binary at {}", + binary.display() + ))?; + + if let Some(stderr) = child.stderr.take() { + std::thread::spawn(move || { + let reader = BufReader::new(stderr); + for line in reader.lines().map_while(Result::ok) { + if !line.is_empty() { + log::debug!("axum: {line}"); + } + } + }); + } + + let handle = AxumHandle { child }; + let base_url = format!("http://127.0.0.1:{port}"); + + // The Axum dev server returns 403 at root (no publisher config in test env), + // so we poll until we get any HTTP response rather than a specific status. + wait_for_any_response(&base_url)?; + + Ok(RuntimeProcess { + inner: Box::new(handle), + base_url, + }) + } + + fn health_check_path(&self) -> &str { + "/health" + } +} + +impl AxumDevServer { + /// Resolve the path to the compiled `trusted-server-axum` binary. + /// + /// Respects the `AXUM_BINARY_PATH` environment variable for CI overrides. + /// Falls back to the workspace `target/debug/` directory. + fn binary_path(&self) -> std::path::PathBuf { + if let Ok(path) = std::env::var("AXUM_BINARY_PATH") { + return std::path::PathBuf::from(path); + } + + // CARGO_MANIFEST_DIR is crates/integration-tests → go up two levels to workspace root + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../target/debug/trusted-server-axum") + } +} + +/// Poll until the Axum dev server responds with any HTTP status code. +/// +/// The Axum server returns 403 at root when no publisher config is present, +/// which is neither success nor 404, so the standard [`super::wait_for_ready`] +/// helper cannot be used. Any HTTP response means the server is up. +fn wait_for_any_response(base_url: &str) -> TestResult<()> { + use error_stack::Report; + + let url = format!("{base_url}/"); + for _ in 0..30 { + if reqwest::blocking::get(&url).is_ok() { + return Ok(()); + } + std::thread::sleep(std::time::Duration::from_millis(500)); + } + Err(Report::new(TestError::RuntimeNotReady) + .attach(format!("Axum dev server at {base_url} not ready after 15s"))) +} + +/// Process handle for a running Axum dev-server instance. +/// +/// Implements [`Drop`] to ensure the process is killed on test cleanup. +struct AxumHandle { + child: Child, +} + +impl RuntimeProcessHandle for AxumHandle {} + +impl Drop for AxumHandle { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} diff --git a/crates/integration-tests/tests/environments/mod.rs b/crates/integration-tests/tests/environments/mod.rs index b53fa4bc..c3797e20 100644 --- a/crates/integration-tests/tests/environments/mod.rs +++ b/crates/integration-tests/tests/environments/mod.rs @@ -1,3 +1,4 @@ +pub mod axum; pub mod fastly; use crate::common::runtime::{RuntimeEnvironment, TestError, TestResult}; @@ -18,7 +19,10 @@ type RuntimeFactory = fn() -> Box; /// 1. Create `tests/environments/.rs` /// 2. Implement [`RuntimeEnvironment`] trait /// 3. Add factory closure here -pub static RUNTIME_ENVIRONMENTS: &[RuntimeFactory] = &[|| Box::new(fastly::FastlyViceroy)]; +pub static RUNTIME_ENVIRONMENTS: &[RuntimeFactory] = &[ + || Box::new(fastly::FastlyViceroy), + || Box::new(axum::AxumDevServer), +]; /// Readiness polling configuration for runtimes and frontend containers. pub(crate) struct ReadyCheckOptions { diff --git a/crates/integration-tests/tests/integration.rs b/crates/integration-tests/tests/integration.rs index e52d0944..288f1685 100644 --- a/crates/integration-tests/tests/integration.rs +++ b/crates/integration-tests/tests/integration.rs @@ -134,3 +134,19 @@ fn test_nextjs_fastly() { let framework = frameworks::nextjs::NextJs; test_combination(&runtime, &framework).expect("should pass Next.js on Fastly"); } + +#[test] +#[ignore = "requires Docker and pre-built trusted-server-axum binary"] +fn test_wordpress_axum() { + let runtime = environments::axum::AxumDevServer; + let framework = frameworks::wordpress::WordPress; + test_combination(&runtime, &framework).expect("should pass WordPress on Axum"); +} + +#[test] +#[ignore = "requires Docker and pre-built trusted-server-axum binary"] +fn test_nextjs_axum() { + let runtime = environments::axum::AxumDevServer; + let framework = frameworks::nextjs::NextJs; + test_combination(&runtime, &framework).expect("should pass Next.js on Axum"); +} diff --git a/crates/trusted-server-adapter-axum/Cargo.toml b/crates/trusted-server-adapter-axum/Cargo.toml new file mode 100644 index 00000000..74c001ea --- /dev/null +++ b/crates/trusted-server-adapter-axum/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "trusted-server-adapter-axum" +version = "0.1.0" +edition = "2024" +publish = false + +[lints] +workspace = true + +[lib] +name = "trusted_server_adapter_axum" +path = "src/lib.rs" + +[[bin]] +name = "trusted-server-axum" +path = "src/main.rs" + +[dependencies] +async-trait = { workspace = true } +edgezero-adapter-axum = { workspace = true, features = ["axum"] } +edgezero-core = { workspace = true } +error-stack = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +simple_logger = "5" +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync", "time"] } +trusted-server-core = { path = "../trusted-server-core" } + +[dev-dependencies] +axum = "0.8" +temp-env = { workspace = true } +edgezero-adapter-axum = { workspace = true, features = ["axum"] } +edgezero-core = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tower = { version = "0.4", features = ["util"] } diff --git a/crates/trusted-server-adapter-axum/axum.toml b/crates/trusted-server-adapter-axum/axum.toml new file mode 100644 index 00000000..48224aa7 --- /dev/null +++ b/crates/trusted-server-adapter-axum/axum.toml @@ -0,0 +1,8 @@ +[app] +name = "trusted-server" +version = "0.1.0" +kind = "http" + +[adapters.axum.logging] +level = "info" +echo_stdout = true diff --git a/crates/trusted-server-adapter-axum/src/app.rs b/crates/trusted-server-adapter-axum/src/app.rs new file mode 100644 index 00000000..3da6b46d --- /dev/null +++ b/crates/trusted-server-adapter-axum/src/app.rs @@ -0,0 +1,335 @@ +use std::sync::Arc; + +use edgezero_core::app::Hooks; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{HeaderValue, Response, header}; +use edgezero_core::router::RouterService; +use error_stack::Report; +use trusted_server_core::auction::endpoints::handle_auction; +use trusted_server_core::auction::{AuctionOrchestrator, build_orchestrator}; +use trusted_server_core::error::{IntoHttpResponse as _, TrustedServerError}; +use trusted_server_core::integrations::IntegrationRegistry; +use trusted_server_core::proxy::{ + handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild, + handle_first_party_proxy_sign, +}; +use trusted_server_core::publisher::{handle_publisher_request, handle_tsjs_dynamic}; +use trusted_server_core::request_signing::{ + handle_deactivate_key, handle_rotate_key, handle_trusted_server_discovery, + handle_verify_signature, +}; +use trusted_server_core::settings::Settings; +use trusted_server_core::settings_data::get_settings; + +use crate::middleware::{AuthMiddleware, FinalizeResponseMiddleware}; +use crate::platform::build_runtime_services; + +// --------------------------------------------------------------------------- +// AppState +// --------------------------------------------------------------------------- + +/// Application state built once at startup and shared across all requests. +pub struct AppState { + settings: Arc, + orchestrator: Arc, + registry: Arc, +} + +/// Build the application state, loading settings and constructing all per-application components. +/// +/// # Errors +/// +/// Returns an error when settings, the auction orchestrator, or the integration +/// registry fail to initialise. +fn build_state() -> Result, Report> { + let settings = get_settings()?; + let orchestrator = build_orchestrator(&settings)?; + let registry = IntegrationRegistry::new(&settings)?; + + Ok(Arc::new(AppState { + settings: Arc::new(settings), + orchestrator: Arc::new(orchestrator), + registry: Arc::new(registry), + })) +} + +// --------------------------------------------------------------------------- +// Error helper +// --------------------------------------------------------------------------- + +/// Convert a [`Report`] into an HTTP [`Response`]. +pub(crate) fn http_error(report: &Report) -> Response { + let root_error = report.current_context(); + log::error!("Error occurred: {:?}", report); + + let body = edgezero_core::body::Body::from(format!("{}\n", root_error.user_message())); + let mut response = Response::new(body); + *response.status_mut() = root_error.status_code(); + response.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + response +} + +// --------------------------------------------------------------------------- +// Startup error fallback +// --------------------------------------------------------------------------- + +/// Returns a [`RouterService`] that responds to every route with the startup error. +fn startup_error_router(e: &Report) -> RouterService { + let message = Arc::new(format!("{}\n", e.current_context().user_message())); + let status = e.current_context().status_code(); + + let make = move |msg: Arc| { + move |_ctx: RequestContext| { + let body = edgezero_core::body::Body::from((*msg).clone()); + let mut resp = Response::new(body); + *resp.status_mut() = status; + resp.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=utf-8"), + ); + async move { Ok::(resp) } + } + }; + + RouterService::builder() + .middleware(FinalizeResponseMiddleware::new(Arc::new( + Settings::default(), + ))) + .get("/", make(Arc::clone(&message))) + .post("/", make(Arc::clone(&message))) + .get("/{*rest}", make(Arc::clone(&message))) + .post("/{*rest}", make(Arc::clone(&message))) + .build() +} + +// --------------------------------------------------------------------------- +// TrustedServerApp +// --------------------------------------------------------------------------- + +/// `EdgeZero` [`Hooks`] implementation for the Trusted Server application. +pub struct TrustedServerApp; + +impl Hooks for TrustedServerApp { + fn name() -> &'static str { + "TrustedServer" + } + + fn routes() -> RouterService { + let state = match build_state() { + Ok(s) => s, + Err(ref e) => { + log::error!("failed to build application state: {:?}", e); + return startup_error_router(e); + } + }; + + // /.well-known/trusted-server.json + let s = Arc::clone(&state); + let discovery_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_trusted_server_discovery(&s.settings, &services, req) + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /verify-signature + let s = Arc::clone(&state); + let verify_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_verify_signature(&s.settings, &services, req) + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /admin/keys/rotate + let s = Arc::clone(&state); + let rotate_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_rotate_key(&s.settings, &services, req) + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /admin/keys/deactivate + let s = Arc::clone(&state); + let deactivate_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_deactivate_key(&s.settings, &services, req) + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /auction + let s = Arc::clone(&state); + let auction_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok( + handle_auction(&s.settings, &s.orchestrator, &services, None, req) + .await + .unwrap_or_else(|e| http_error(&e)), + ) + } + }; + + // /first-party/proxy + let s = Arc::clone(&state); + let fp_proxy_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_first_party_proxy(&s.settings, &services, req) + .await + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /first-party/click + let s = Arc::clone(&state); + let fp_click_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_first_party_click(&s.settings, &services, req) + .await + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // GET /first-party/sign + let s = Arc::clone(&state); + let fp_sign_get_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_first_party_proxy_sign(&s.settings, &services, req) + .await + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // POST /first-party/sign + let s = Arc::clone(&state); + let fp_sign_post_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok(handle_first_party_proxy_sign(&s.settings, &services, req) + .await + .unwrap_or_else(|e| http_error(&e))) + } + }; + + // /first-party/proxy-rebuild + let s = Arc::clone(&state); + let fp_rebuild_handler = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + Ok( + handle_first_party_proxy_rebuild(&s.settings, &services, req) + .await + .unwrap_or_else(|e| http_error(&e)), + ) + } + }; + + // GET /{*rest} — tsjs, integration proxy, or publisher fallback + let s = Arc::clone(&state); + let get_fallback = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + let path = req.uri().path().to_string(); + let method = req.method().clone(); + + let result = if path.starts_with("/static/tsjs=") { + handle_tsjs_dynamic(&req, &s.registry) + } else if s.registry.has_route(&method, &path) { + s.registry + .handle_proxy(&method, &path, &s.settings, &services, req) + .await + .unwrap_or_else(|| { + Err(Report::new(TrustedServerError::BadRequest { + message: format!("Unknown integration route: {path}"), + })) + }) + } else { + handle_publisher_request(&s.settings, &s.registry, &services, None, req).await + }; + + Ok(result.unwrap_or_else(|e| http_error(&e))) + } + }; + + // POST /{*rest} — integration proxy or publisher origin fallback + let s = Arc::clone(&state); + let post_fallback = move |ctx: RequestContext| { + let s = Arc::clone(&s); + async move { + let services = build_runtime_services(&ctx); + let req = ctx.into_request(); + let path = req.uri().path().to_string(); + let method = req.method().clone(); + + let result = if s.registry.has_route(&method, &path) { + s.registry + .handle_proxy(&method, &path, &s.settings, &services, req) + .await + .unwrap_or_else(|| { + Err(Report::new(TrustedServerError::BadRequest { + message: format!("Unknown integration route: {path}"), + })) + }) + } else { + handle_publisher_request(&s.settings, &s.registry, &services, None, req).await + }; + + Ok(result.unwrap_or_else(|e| http_error(&e))) + } + }; + + RouterService::builder() + .middleware(FinalizeResponseMiddleware::new(Arc::clone(&state.settings))) + .middleware(AuthMiddleware::new(Arc::clone(&state.settings))) + .get("/.well-known/trusted-server.json", discovery_handler) + .post("/verify-signature", verify_handler) + .post("/admin/keys/rotate", rotate_handler) + .post("/admin/keys/deactivate", deactivate_handler) + .post("/auction", auction_handler) + .get("/first-party/proxy", fp_proxy_handler) + .get("/first-party/click", fp_click_handler) + .get("/first-party/sign", fp_sign_get_handler) + .post("/first-party/sign", fp_sign_post_handler) + .post("/first-party/proxy-rebuild", fp_rebuild_handler) + .get("/", get_fallback.clone()) + .post("/", post_fallback.clone()) + .get("/{*rest}", get_fallback) + .post("/{*rest}", post_fallback) + .build() + } +} diff --git a/crates/trusted-server-adapter-axum/src/lib.rs b/crates/trusted-server-adapter-axum/src/lib.rs new file mode 100644 index 00000000..825e0542 --- /dev/null +++ b/crates/trusted-server-adapter-axum/src/lib.rs @@ -0,0 +1,3 @@ +pub mod app; +pub mod middleware; +pub mod platform; diff --git a/crates/trusted-server-adapter-axum/src/main.rs b/crates/trusted-server-adapter-axum/src/main.rs new file mode 100644 index 00000000..7620e187 --- /dev/null +++ b/crates/trusted-server-adapter-axum/src/main.rs @@ -0,0 +1,39 @@ +use edgezero_core::app::Hooks as _; +use trusted_server_adapter_axum::app::TrustedServerApp; + +fn main() { + // When PORT is set, use a dynamic address so integration tests can allocate + // a fresh OS port each run and avoid TIME_WAIT flakiness. The standard + // `run_app` path is kept for normal development (reads config from axum.toml). + if let Some(port) = port_from_env() { + let _ = simple_logger::SimpleLogger::new().init(); + let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port)); + let config = edgezero_adapter_axum::AxumDevServerConfig { + addr, + enable_ctrl_c: true, + }; + let router = TrustedServerApp::routes(); + if let Err(err) = edgezero_adapter_axum::AxumDevServer::with_config(router, config).run() { + log::error!("trusted-server-adapter-axum failed: {err}"); + std::process::exit(1); + } + } else if let Err(err) = + edgezero_adapter_axum::run_app::(include_str!("../axum.toml")) + { + log::error!("trusted-server-adapter-axum failed: {err}"); + std::process::exit(1); + } +} + +/// Read a port number from the `PORT` environment variable. +/// +/// Returns `None` when the variable is unset or cannot be parsed as `u16`. +fn port_from_env() -> Option { + std::env::var("PORT").ok()?.parse().ok() +} + +#[cfg(test)] +mod tests { + #[test] + fn crate_compiles() {} +} diff --git a/crates/trusted-server-adapter-axum/src/middleware.rs b/crates/trusted-server-adapter-axum/src/middleware.rs new file mode 100644 index 00000000..1bb9d860 --- /dev/null +++ b/crates/trusted-server-adapter-axum/src/middleware.rs @@ -0,0 +1,191 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use edgezero_core::context::RequestContext; +use edgezero_core::error::EdgeError; +use edgezero_core::http::{HeaderName, HeaderValue, Response}; +use edgezero_core::middleware::{Middleware, Next}; +use trusted_server_core::auth::enforce_basic_auth; +use trusted_server_core::constants::HEADER_X_GEO_INFO_AVAILABLE; +use trusted_server_core::settings::Settings; + +// --------------------------------------------------------------------------- +// FinalizeResponseMiddleware +// --------------------------------------------------------------------------- + +/// Outermost middleware: injects all standard TS response headers. +/// +/// Geo lookup is unavailable in the Axum dev server — `X-Geo-Info-Available: false` +/// is always emitted. Fastly-specific headers (`X-TS-Version`, `X-TS-ENV`) are +/// skipped because the corresponding env vars are not set in a local dev context. +/// +/// Registered first in the middleware chain so that every outgoing response — +/// including auth-rejected ones — carries a consistent set of headers. +pub struct FinalizeResponseMiddleware { + settings: Arc, +} + +impl FinalizeResponseMiddleware { + /// Creates a new [`FinalizeResponseMiddleware`] with the given settings. + #[must_use] + pub fn new(settings: Arc) -> Self { + Self { settings } + } +} + +#[async_trait(?Send)] +impl Middleware for FinalizeResponseMiddleware { + async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { + let mut response = next.run(ctx).await?; + apply_finalize_headers(&self.settings, &mut response); + Ok(response) + } +} + +// --------------------------------------------------------------------------- +// AuthMiddleware +// --------------------------------------------------------------------------- + +/// Inner middleware: enforces basic-auth before the handler runs. +/// +/// - `Ok(Some(response))` from [`enforce_basic_auth`] → auth failed; return the +/// challenge response (bubbles through [`FinalizeResponseMiddleware`] for header injection). +/// - `Ok(None)` → no auth required or credentials accepted; continue the chain. +/// - `Err(report)` → internal error; log and convert to a 500 HTTP response. +pub struct AuthMiddleware { + settings: Arc, +} + +impl AuthMiddleware { + /// Creates a new [`AuthMiddleware`] with the given settings. + #[must_use] + pub fn new(settings: Arc) -> Self { + Self { settings } + } +} + +#[async_trait(?Send)] +impl Middleware for AuthMiddleware { + async fn handle(&self, ctx: RequestContext, next: Next<'_>) -> Result { + match enforce_basic_auth(&self.settings, ctx.request()) { + Ok(Some(response)) => return Ok(response), + Ok(None) => {} + Err(report) => { + log::error!("auth check failed: {:?}", report); + return Ok(crate::app::http_error(&report)); + } + } + + next.run(ctx).await + } +} + +// --------------------------------------------------------------------------- +// apply_finalize_headers — extracted for unit testing +// --------------------------------------------------------------------------- + +/// Applies standard Trusted Server response headers to the given response. +/// +/// Unlike the Fastly variant, geo is always unavailable so `X-Geo-Info-Available: false` +/// is unconditionally emitted. Fastly-specific headers are omitted. +/// Operator-configured `settings.response_headers` are applied last and can override +/// any managed header. +pub(crate) fn apply_finalize_headers(settings: &Settings, response: &mut Response) { + response.headers_mut().insert( + HEADER_X_GEO_INFO_AVAILABLE, + HeaderValue::from_static("false"), + ); + + for (key, value) in &settings.response_headers { + let header_name = HeaderName::from_bytes(key.as_bytes()); + let header_value = HeaderValue::from_str(value); + if let (Ok(header_name), Ok(header_value)) = (header_name, header_value) { + response.headers_mut().insert(header_name, header_value); + } else { + log::warn!( + "Skipping invalid configured response header value for {}", + key + ); + } + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + use edgezero_core::body::Body; + use edgezero_core::http::response_builder; + + fn empty_response() -> Response { + response_builder() + .body(Body::empty()) + .expect("should build empty test response") + } + + fn settings_with_response_headers(headers: Vec<(&str, &str)>) -> Settings { + let mut s = + trusted_server_core::settings_data::get_settings().expect("should load test settings"); + s.response_headers = headers + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + s + } + + #[test] + fn sets_geo_unavailable_header() { + let settings = settings_with_response_headers(vec![]); + let mut response = empty_response(); + + apply_finalize_headers(&settings, &mut response); + + assert_eq!( + response + .headers() + .get("x-geo-info-available") + .and_then(|v| v.to_str().ok()), + Some("false"), + "should set X-Geo-Info-Available: false" + ); + } + + #[test] + fn operator_response_headers_override_geo_header() { + let settings = + settings_with_response_headers(vec![("X-Geo-Info-Available", "operator-override")]); + let mut response = empty_response(); + + apply_finalize_headers(&settings, &mut response); + + assert_eq!( + response + .headers() + .get("x-geo-info-available") + .and_then(|v| v.to_str().ok()), + Some("operator-override"), + "should override the managed geo header with the operator-configured value" + ); + } + + #[test] + fn applies_custom_operator_headers() { + let settings = settings_with_response_headers(vec![("X-Custom-Header", "custom-value")]); + let mut response = empty_response(); + + apply_finalize_headers(&settings, &mut response); + + assert_eq!( + response + .headers() + .get("x-custom-header") + .and_then(|v| v.to_str().ok()), + Some("custom-value"), + "should apply operator-configured response headers" + ); + } +} diff --git a/crates/trusted-server-adapter-axum/src/platform.rs b/crates/trusted-server-adapter-axum/src/platform.rs new file mode 100644 index 00000000..e1b88521 --- /dev/null +++ b/crates/trusted-server-adapter-axum/src/platform.rs @@ -0,0 +1,503 @@ +use std::net::IpAddr; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use error_stack::{Report, ResultExt as _}; +use trusted_server_core::platform::{ + ClientInfo, GeoInfo, PlatformBackend, PlatformBackendSpec, PlatformConfigStore, PlatformError, + PlatformGeo, PlatformHttpClient, PlatformHttpRequest, PlatformPendingRequest, PlatformResponse, + PlatformSecretStore, PlatformSelectResult, RuntimeServices, StoreId, StoreName, +}; + +// --------------------------------------------------------------------------- +// Env-var naming helpers +// --------------------------------------------------------------------------- + +/// Normalize a store name or key for use as an environment-variable segment. +/// +/// Uppercases and replaces hyphens, dots, and spaces with underscores. +fn normalize_env_segment(s: &str) -> String { + s.to_uppercase().replace(['-', '.', ' '], "_") +} + +fn config_env_var(store_name: &str, key: &str) -> String { + format!( + "TRUSTED_SERVER_CONFIG_{}_{}", + normalize_env_segment(store_name), + normalize_env_segment(key), + ) +} + +fn secret_env_var(store_name: &str, key: &str) -> String { + format!( + "TRUSTED_SERVER_SECRET_{}_{}", + normalize_env_segment(store_name), + normalize_env_segment(key), + ) +} + +// --------------------------------------------------------------------------- +// PlatformConfigStore +// --------------------------------------------------------------------------- + +/// Environment-variable–backed config store for the Axum dev server. +/// +/// Reads from `TRUSTED_SERVER_CONFIG_{STORE}_{KEY}` (uppercased, hyphens→underscores). +/// Write operations are unsupported in local development. +pub struct AxumPlatformConfigStore; + +impl PlatformConfigStore for AxumPlatformConfigStore { + fn get(&self, store_name: &StoreName, key: &str) -> Result> { + let var_name = config_env_var(store_name.as_ref(), key); + std::env::var(&var_name).map_err(|_| { + Report::new(PlatformError::ConfigStore).attach(format!( + "env var '{var_name}' not set — export it to supply this config value" + )) + }) + } + + fn put( + &self, + store_id: &StoreId, + key: &str, + _value: &str, + ) -> Result<(), Report> { + log::warn!( + "AxumPlatformConfigStore: write to store '{}' key '{}' ignored \ + (config store writes are not supported on the Axum dev server)", + store_id.as_ref(), + key + ); + Err(Report::new(PlatformError::ConfigStore) + .attach("config store writes are not supported on the Axum dev server")) + } + + fn delete(&self, store_id: &StoreId, key: &str) -> Result<(), Report> { + log::warn!( + "AxumPlatformConfigStore: delete from store '{}' key '{}' ignored \ + (config store deletes are not supported on the Axum dev server)", + store_id.as_ref(), + key + ); + Err(Report::new(PlatformError::ConfigStore) + .attach("config store deletes are not supported on the Axum dev server")) + } +} + +// --------------------------------------------------------------------------- +// PlatformSecretStore +// --------------------------------------------------------------------------- + +/// Environment-variable–backed secret store for the Axum dev server. +/// +/// Reads from `TRUSTED_SERVER_SECRET_{STORE}_{KEY}` as raw UTF-8 bytes. +/// Write operations are unsupported in local development. +pub struct AxumPlatformSecretStore; + +impl PlatformSecretStore for AxumPlatformSecretStore { + fn get_bytes( + &self, + store_name: &StoreName, + key: &str, + ) -> Result, Report> { + let var_name = secret_env_var(store_name.as_ref(), key); + std::env::var(&var_name) + .map(String::into_bytes) + .map_err(|_| { + Report::new(PlatformError::SecretStore).attach(format!( + "env var '{var_name}' not set — export it to supply this secret value" + )) + }) + } + + fn create( + &self, + store_id: &StoreId, + name: &str, + _value: &str, + ) -> Result<(), Report> { + log::warn!( + "AxumPlatformSecretStore: create '{}' in store '{}' ignored \ + (secret store writes are not supported on the Axum dev server)", + name, + store_id.as_ref() + ); + Err(Report::new(PlatformError::SecretStore) + .attach("secret store writes are not supported on the Axum dev server")) + } + + fn delete(&self, store_id: &StoreId, name: &str) -> Result<(), Report> { + log::warn!( + "AxumPlatformSecretStore: delete '{}' from store '{}' ignored \ + (secret store deletes are not supported on the Axum dev server)", + name, + store_id.as_ref() + ); + Err(Report::new(PlatformError::SecretStore) + .attach("secret store deletes are not supported on the Axum dev server")) + } +} + +// --------------------------------------------------------------------------- +// PlatformBackend +// --------------------------------------------------------------------------- + +/// No-op backend for the Axum dev server. +/// +/// Returns a deterministic name; `ensure` is a no-op returning the same name. +/// The Axum HTTP client sends directly to URIs and ignores backend names. +pub struct AxumPlatformBackend; + +impl PlatformBackend for AxumPlatformBackend { + fn predict_name(&self, spec: &PlatformBackendSpec) -> Result> { + let port = spec + .port + .unwrap_or(if spec.scheme == "https" { 443 } else { 80 }); + Ok(format!( + "{}_{}_{}", + normalize_env_segment(&spec.scheme), + normalize_env_segment(&spec.host), + port, + )) + } + + fn ensure(&self, spec: &PlatformBackendSpec) -> Result> { + self.predict_name(spec) + } +} + +// --------------------------------------------------------------------------- +// PlatformGeo +// --------------------------------------------------------------------------- + +/// No-op geo implementation — geographic lookup is unavailable in local development. +pub struct AxumPlatformGeo; + +impl PlatformGeo for AxumPlatformGeo { + fn lookup(&self, _client_ip: Option) -> Result, Report> { + Ok(None) + } +} + +// --------------------------------------------------------------------------- +// PlatformHttpClient +// --------------------------------------------------------------------------- + +/// Raw response parts carried through `send_async` → `select`. +/// +/// Stores `(status, headers, body)` instead of `PlatformResponse` because +/// `Body::Stream` is `!Send`, making it incompatible with `Box` +/// required by [`PlatformPendingRequest`]. Same pattern as `StubPendingResponse` +/// in `trusted-server-core::platform::test_support`. +struct AxumPendingResponse { + backend_name: String, + status: u16, + headers: Vec<(String, Vec)>, + body: Vec, +} + +/// reqwest-backed HTTP client for the Axum dev server. +/// +/// `send_async` + `select` use eager sequential evaluation acceptable for local +/// development. True async fan-out is not needed at dev-server scale. +pub struct AxumPlatformHttpClient { + client: reqwest::Client, +} + +impl AxumPlatformHttpClient { + /// Create a new client with sensible dev-server timeouts. + /// + /// # Panics + /// + /// Panics if the underlying `reqwest::Client` cannot be built (should not + /// happen with the default TLS configuration on any supported platform). + #[must_use] + pub fn new() -> Self { + Self { + client: reqwest::Client::builder() + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(30)) + .build() + .expect("should build reqwest client"), + } + } + + async fn execute( + &self, + request: PlatformHttpRequest, + ) -> Result> { + let uri = request.request.uri().to_string(); + let method = reqwest::Method::from_bytes(request.request.method().as_str().as_bytes()) + .change_context(PlatformError::HttpClient)?; + + let mut builder = self.client.request(method, &uri); + for (name, value) in request.request.headers() { + builder = builder.header(name.as_str(), value.as_bytes()); + } + + let (_, body) = request.request.into_parts(); + match body { + edgezero_core::body::Body::Once(bytes) => { + if !bytes.is_empty() { + builder = builder.body(bytes); + } + } + edgezero_core::body::Body::Stream(_) => { + log::warn!( + "AxumPlatformHttpClient: Body::Stream is not supported; \ + outbound request body will be empty" + ); + } + } + + let resp = builder + .send() + .await + .change_context(PlatformError::HttpClient) + .attach(format!("outbound request to {uri} failed"))?; + + let status = resp.status().as_u16(); + let mut edge_builder = edgezero_core::http::response_builder().status(status); + for (name, value) in resp.headers() { + edge_builder = edge_builder.header(name.as_str(), value.as_bytes()); + } + let resp_bytes = resp + .bytes() + .await + .change_context(PlatformError::HttpClient)?; + let edge_resp = edge_builder + .body(edgezero_core::body::Body::from(resp_bytes.to_vec())) + .change_context(PlatformError::HttpClient)?; + + Ok(PlatformResponse::new(edge_resp).with_backend_name(request.backend_name)) + } +} + +impl Default for AxumPlatformHttpClient { + fn default() -> Self { + Self::new() + } +} + +#[async_trait(?Send)] +impl PlatformHttpClient for AxumPlatformHttpClient { + async fn send( + &self, + request: PlatformHttpRequest, + ) -> Result> { + self.execute(request).await + } + + async fn send_async( + &self, + request: PlatformHttpRequest, + ) -> Result> { + // Dev-server divergence: execution is eager — errors surface here, not at + // select() time. On Fastly the request is in-flight until select() resolves it. + log::debug!( + "AxumPlatformHttpClient::send_async: executing eagerly (Fastly surfaces errors at select)" + ); + let backend_name = request.backend_name.clone(); + let response = self.execute(request).await?; + + let status = response.response.status().as_u16(); + let headers: Vec<(String, Vec)> = response + .response + .headers() + .iter() + .map(|(n, v)| (n.to_string(), v.as_bytes().to_vec())) + .collect(); + let body_bytes = match response.response.into_body() { + edgezero_core::body::Body::Once(bytes) => bytes.to_vec(), + edgezero_core::body::Body::Stream(_) => vec![], + }; + + let pending = AxumPendingResponse { + backend_name: backend_name.clone(), + status, + headers, + body: body_bytes, + }; + Ok(PlatformPendingRequest::new(pending).with_backend_name(backend_name)) + } + + async fn select( + &self, + mut pending_requests: Vec, + ) -> Result> { + if pending_requests.is_empty() { + return Err(Report::new(PlatformError::HttpClient) + .attach("select called with an empty pending_requests list")); + } + + // Dev-server divergence: pops index 0 unconditionally — not "first to complete". + // Safe here because send_async already ran eagerly, but any test verifying + // parallel fan-out ordering against the Fastly runtime should use a real + // Fastly environment. + log::debug!( + "AxumPlatformHttpClient::select: returning index 0 (sequential, not parallel fan-out)" + ); + let ready_platform = pending_requests.remove(0); + let pending = ready_platform + .downcast::() + .map_err(|_| { + Report::new(PlatformError::HttpClient) + .attach("unexpected inner type in AxumPlatformHttpClient::select") + })?; + + let mut builder = edgezero_core::http::response_builder().status(pending.status); + for (name, value) in &pending.headers { + builder = builder.header(name.as_str(), value.as_slice()); + } + let edge_resp = builder + .body(edgezero_core::body::Body::from(pending.body)) + .change_context(PlatformError::HttpClient)?; + + let ready = Ok(PlatformResponse::new(edge_resp).with_backend_name(pending.backend_name)); + Ok(PlatformSelectResult { + ready, + remaining: pending_requests, + }) + } +} + +// --------------------------------------------------------------------------- +// build_runtime_services +// --------------------------------------------------------------------------- + +/// Construct [`RuntimeServices`] for an incoming Axum request. +/// +/// # Degraded features in dev +/// +/// KV store is [`trusted_server_core::platform::UnavailableKvStore`] — any route +/// touching synthetic-ID or consent KV will degrade gracefully. A `warn` log is +/// emitted once per process. +pub fn build_runtime_services(ctx: &edgezero_core::context::RequestContext) -> RuntimeServices { + static KV_WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new(); + KV_WARNED.get_or_init(|| { + log::warn!( + "Axum dev server: KV store is unavailable (UnavailableKvStore). \ + Routes that depend on synthetic-ID or consent KV will degrade gracefully." + ); + }); + + let client_ip = edgezero_adapter_axum::AxumRequestContext::get(ctx.request()) + .and_then(|c| c.remote_addr) + .map(|addr| addr.ip()); + + RuntimeServices::builder() + .config_store(Arc::new(AxumPlatformConfigStore)) + .secret_store(Arc::new(AxumPlatformSecretStore)) + .kv_store(Arc::new(trusted_server_core::platform::UnavailableKvStore)) + .backend(Arc::new(AxumPlatformBackend)) + // Keep the HTTP client request-scoped in the dev adapter. Axum still + // drops outbound `Body::Stream` requests, and sharing a pooled client + // across requests regressed the Next.js server-action -> API-route + // integration flow by reusing a poisoned connection after the truncated + // POST. Revisit pooling only after streamed request bodies are handled. + .http_client(Arc::new(AxumPlatformHttpClient::new())) + .geo(Arc::new(AxumPlatformGeo)) + .client_info(ClientInfo { + client_ip, + tls_protocol: None, + tls_cipher: None, + }) + .build() +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + #[test] + fn config_store_reads_from_env_var() { + temp_env::with_var( + "TRUSTED_SERVER_CONFIG_MY_STORE_MY_KEY", + Some("test-value"), + || { + let store = AxumPlatformConfigStore; + let result = store + .get(&StoreName::from("my-store"), "my-key") + .expect("should read env var"); + assert_eq!(result, "test-value", "should return env var value"); + }, + ); + } + + #[test] + fn config_store_returns_error_for_missing_env_var() { + let store = AxumPlatformConfigStore; + let result = store.get( + &StoreName::from("nonexistent-store-zzz"), + "nonexistent-key-zzz", + ); + assert!(result.is_err(), "should error for missing env var"); + } + + #[test] + fn secret_store_reads_bytes_from_env_var() { + temp_env::with_var( + "TRUSTED_SERVER_SECRET_MY_SECRETS_MY_SECRET", + Some("hello"), + || { + let store = AxumPlatformSecretStore; + let result = store + .get_bytes(&StoreName::from("my-secrets"), "my-secret") + .expect("should read env var as bytes"); + assert_eq!(result, b"hello", "should return raw bytes"); + }, + ); + } + + #[test] + fn backend_predict_name_returns_deterministic_string() { + let backend = AxumPlatformBackend; + let spec = PlatformBackendSpec { + scheme: "https".to_string(), + host: "example.com".to_string(), + port: None, + certificate_check: true, + first_byte_timeout: Duration::from_secs(15), + }; + let name1 = backend.predict_name(&spec).expect("should return a name"); + let name2 = backend + .predict_name(&spec) + .expect("should return same name"); + assert!(!name1.is_empty(), "should return a non-empty name"); + assert_eq!(name1, name2, "should be deterministic"); + } + + #[test] + fn backend_ensure_returns_same_name_as_predict() { + let backend = AxumPlatformBackend; + let spec = PlatformBackendSpec { + scheme: "https".to_string(), + host: "example.com".to_string(), + port: None, + certificate_check: true, + first_byte_timeout: Duration::from_secs(15), + }; + assert_eq!( + backend.predict_name(&spec).expect("should return name"), + backend.ensure(&spec).expect("should return name"), + "ensure should equal predict_name" + ); + } + + #[test] + fn geo_always_returns_none() { + let geo = AxumPlatformGeo; + let no_ip = geo.lookup(None).expect("should not error"); + assert!(no_ip.is_none(), "should return None for no IP"); + let with_ip = geo + .lookup(Some("127.0.0.1".parse().expect("should parse IP"))) + .expect("should not error"); + assert!(with_ip.is_none(), "should return None for any IP"); + } +} diff --git a/crates/trusted-server-adapter-axum/tests/routes.rs b/crates/trusted-server-adapter-axum/tests/routes.rs new file mode 100644 index 00000000..eaed9c3c --- /dev/null +++ b/crates/trusted-server-adapter-axum/tests/routes.rs @@ -0,0 +1,244 @@ +//! Integration tests for the Axum dev server. +//! +//! Uses `EdgeZeroAxumService` directly (no live TCP server) so tests remain fast +//! and self-contained. Each test builds the full `TrustedServerApp` router and +//! drives it through the Tower `Service` interface. + +use axum::body::Body as AxumBody; +use axum::http::Request; +use edgezero_adapter_axum::EdgeZeroAxumService; +use edgezero_core::app::Hooks as _; +use tower::{Service as _, ServiceExt as _}; +use trusted_server_adapter_axum::app::TrustedServerApp; + +fn make_service() -> EdgeZeroAxumService { + EdgeZeroAxumService::new(TrustedServerApp::routes()) +} + +// --------------------------------------------------------------------------- +// Route smoke tests — verify routing (not business logic correctness) +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn discovery_endpoint_is_routed() { + // Verifies the route exists — 5xx from missing signing keys is acceptable; + // 404 is not (that would mean the route was not registered). + 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(), + 404, + "discovery endpoint must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn verify_signature_endpoint_is_routed() { + let mut svc = make_service(); + + let req = Request::builder() + .method("POST") + .uri("/verify-signature") + .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, + "verify-signature must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_is_routed() { + 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_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 mut svc = make_service(); + + let req = Request::builder() + .method("POST") + .uri("/admin/keys/deactivate") + .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, + "admin/keys/deactivate must be routed" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_rotate_key_returns_non_5xx() { + // Config store write rejection must produce a structured 4xx, not 5xx/panic. + 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"); + let status = resp.status().as_u16(); + + assert_ne!(status, 404, "admin/keys/rotate must be routed"); + assert!( + status < 500, + "config store write rejection must produce 4xx, not 5xx: got {status}" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn tsjs_route_prefix_is_handled_not_5xx() { + let mut svc = make_service(); + + // /static/tsjs= is a GET /{*rest} catch-all path. The handler returns 404 + // for an unknown hash, which is correct application behaviour (not a routing 404). + // This test verifies the handler is reached (no 5xx/panic) and that routing works. + let req = Request::builder() + .method("GET") + .uri("/static/tsjs=0000000000000000") + .body(AxumBody::empty()) + .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!( + status < 500, + "tsjs catch-all handler must not return 5xx: got {status}" + ); +} + +// --------------------------------------------------------------------------- +// Middleware tests +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn finalize_middleware_sets_geo_unavailable_header() { + let mut svc = make_service(); + + let req = Request::builder() + .method("POST") + .uri("/verify-signature") + .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.headers() + .get("x-geo-info-available") + .and_then(|v| v.to_str().ok()), + Some("false"), + "finalize middleware should set X-Geo-Info-Available: false on every response" + ); +} + +// --------------------------------------------------------------------------- +// Basic-auth gate test +// --------------------------------------------------------------------------- + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn admin_route_returns_non_404_non_5xx() { + 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"); + let status = resp.status().as_u16(); + + assert_ne!(status, 404, "admin route must be routed"); + assert!( + status < 500, + "admin route should not return 5xx: got {status}" + ); +} diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index 76922248..07c8f8a1 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -4,7 +4,7 @@ Understanding the architecture of Trusted Server. ## High-Level Overview -Trusted Server is built as a Rust-based edge computing application that runs on Fastly Compute platform. +Trusted Server is built as a Rust-based edge computing application. The core logic lives in a platform-agnostic library; platform-specific adapters target different runtimes (Fastly Compute, native Axum). ```mermaid flowchart TD @@ -37,12 +37,20 @@ Core library containing shared functionality: ### trusted-server-adapter-fastly -Fastly-specific implementation: +Fastly Compute adapter (WASM binary, `wasm32-wasip1` target): -- Main application entry point -- Fastly SDK integration -- Request/response handling -- KV store access +- Main application entry point for production Fastly deployment +- Fastly SDK integration (KV stores, secret stores, geo lookup) +- Compiled to WebAssembly and run via Viceroy locally or on Fastly's edge + +### trusted-server-adapter-axum + +Native Axum dev server adapter (native binary): + +- Runs the full trusted-server pipeline locally without Fastly or Viceroy +- Platform implementations backed by environment variables instead of Fastly stores +- Useful for rapid local development, integration testing, and non-Fastly deployments +- Listens on `http://localhost:8787` by default ## Design Patterns @@ -105,13 +113,14 @@ User data is not persisted in storage - only processed in-flight at the edge. - **Request Signing** - Optional request authentication - **Content Security** - Creative scanning and modification -## WebAssembly Target +## Runtime Targets -Compiled to `wasm32-wasip1` for Fastly Compute: +| Adapter | Target | Use case | +| ------------------------------- | --------------- | -------------------------------------- | +| `trusted-server-adapter-fastly` | `wasm32-wasip1` | Production on Fastly Compute | +| `trusted-server-adapter-axum` | native | Local development, integration testing | -- Sandboxed execution -- Fast cold starts -- Efficient resource usage +The Fastly adapter compiles to WebAssembly for sandboxed, low-cold-start edge execution. The Axum adapter is a standard native binary — no WASM toolchain required for local development. ## Next Steps diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 47eb250d..e5b2610a 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -7,9 +7,12 @@ Get up and running with Trusted Server quickly. Before you begin, ensure you have: - Rust 1.91.1 (see `.tool-versions`) +- Basic familiarity with Rust and WebAssembly + +**For Fastly deployment** (optional for local dev): + - Fastly CLI installed - A Fastly account and API key -- Basic familiarity with WebAssembly ## Installation @@ -20,37 +23,57 @@ git clone https://github.com/IABTechLab/trusted-server.git cd trusted-server ``` -### Fastly CLI Setup +## Local Development + +Trusted Server supports two local development modes: -Install and configure the Fastly CLI using the [Fastly setup guide](/guide/fastly). +### Option A — Fastly Compute via Viceroy -### Install Viceroy (Test Runtime) +Simulates the full Fastly production environment locally. + +Install and configure the Fastly CLI using the [Fastly setup guide](/guide/fastly), then install Viceroy: ```bash cargo install viceroy ``` -## Local Development - -### Build the Project +Start the local Fastly simulator: ```bash -cargo build +fastly compute serve ``` -### Run Tests +The server will be available at `http://localhost:7676`. + +### Option B — Axum dev server + +No Fastly account, CLI, or Viceroy needed. Runs natively on your machine. ```bash -cargo test +# Copy and edit the environment file +cp .env.dev .env + +# Build and start the dev server +cargo run -p trusted-server-adapter-axum ``` -### Start Local Server +The server will be available at `http://localhost:8787`. + +### Build the Project ```bash -fastly compute serve +cargo build ``` -The server will be available at `http://localhost:7676`. +### Run Tests + +```bash +# Fastly/WASM crates (requires Viceroy) +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 + +# Axum native adapter +cargo test -p trusted-server-adapter-axum +``` ## Configuration diff --git a/docs/guide/testing.md b/docs/guide/testing.md index eac0801d..7e0005b1 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -6,14 +6,22 @@ Learn how to test Trusted Server locally and in CI/CD. ### Viceroy -Viceroy is the local test runtime for Fastly Compute applications. It simulates the Fastly environment locally. +Viceroy is the local test runtime for Fastly Compute applications. It simulates the Fastly environment locally and is required for running the WASM crate tests. ```bash # Install viceroy cargo install viceroy -# Run tests (viceroy is invoked automatically) -cargo test +# Run WASM crate tests (viceroy is invoked automatically) +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 +``` + +### Axum adapter tests + +The Axum adapter runs as a native binary — no Viceroy or WASM toolchain needed: + +```bash +cargo test -p trusted-server-adapter-axum ``` ### Test Organization @@ -47,40 +55,54 @@ mod tests { ### Unit Tests ```bash -# Run all tests -cargo test +# Run all WASM crate tests (requires Viceroy) +cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 -# Run specific test by name -cargo test test_generate_synthetic_id +# Run Axum adapter tests (native, no Viceroy needed) +cargo test -p trusted-server-adapter-axum -# Run tests with output visible -cargo test -- --nocapture - -# Run tests for specific crate +# Run tests for a specific crate cargo test -p trusted-server-core # Run tests matching a pattern cargo test synthetic + +# Show test output +cargo test -- --nocapture ``` ### Integration Tests +The integration test suite runs the full pipeline against Docker containers using both the Fastly (Viceroy) and Axum runtimes: + ```bash -# Run all integration tests -cargo test --test '*' +# Build both runtimes and run all integration tests +./scripts/integration-tests.sh -# Run with single thread (useful for debugging) -cargo test -- --test-threads=1 +# Run a single test +./scripts/integration-tests.sh test_wordpress_axum +./scripts/integration-tests.sh test_wordpress_fastly ``` ### Local Server Testing +**Axum dev server** (no Fastly CLI required): + ```bash -# Start local server +# Start the Axum dev server +cargo run -p trusted-server-adapter-axum + +# Test endpoints with curl +curl http://localhost:8787/.well-known/trusted-server.json +``` + +**Fastly Viceroy** (requires Fastly CLI): + +```bash +# Start local Fastly simulator fastly compute serve # Test endpoints with curl -curl http://localhost:7676/health curl http://localhost:7676/.well-known/trusted-server.json ``` @@ -261,20 +283,29 @@ cargo tarpaulin --out Html Tests run automatically on pull requests and main branch commits. See `.github/workflows/` for the complete CI configuration. ```yaml -# Example workflow +# Example workflow (see .github/workflows/test.yml for the full version) name: Test on: [push, pull_request] jobs: - test: + test-rust: # Fastly/WASM crates — requires Viceroy runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-action@stable - name: Run tests - run: cargo test + run: cargo test --workspace --exclude trusted-server-adapter-axum --target wasm32-wasip1 - name: Run clippy run: cargo clippy --workspace --all-targets --all-features -- -D warnings + + test-axum: # Axum native adapter — no Viceroy needed + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build Axum adapter + run: cargo build -p trusted-server-adapter-axum + - name: Run Axum adapter tests + run: cargo test -p trusted-server-adapter-axum ``` ## Debugging Tests diff --git a/scripts/integration-tests.sh b/scripts/integration-tests.sh index 336236b6..b1a79fca 100755 --- a/scripts/integration-tests.sh +++ b/scripts/integration-tests.sh @@ -50,13 +50,20 @@ if [ -z "$TARGET" ]; then exit 1 fi -echo "==> Building WASM binary (origin=http://127.0.0.1:$ORIGIN_PORT)..." +echo "==> Building Fastly WASM binary (origin=http://127.0.0.1:$ORIGIN_PORT)..." TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="http://127.0.0.1:$ORIGIN_PORT" \ TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="integration-test-proxy-secret" \ TRUSTED_SERVER__SYNTHETIC__SECRET_KEY="integration-test-secret-key" \ TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK=false \ cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1 +echo "==> Building Axum native binary (origin=http://127.0.0.1:$ORIGIN_PORT)..." +TRUSTED_SERVER__PUBLISHER__ORIGIN_URL="http://127.0.0.1:$ORIGIN_PORT" \ +TRUSTED_SERVER__PUBLISHER__PROXY_SECRET="integration-test-proxy-secret" \ +TRUSTED_SERVER__SYNTHETIC__SECRET_KEY="integration-test-secret-key" \ +TRUSTED_SERVER__PROXY__CERTIFICATE_CHECK=false \ + cargo build -p trusted-server-adapter-axum + echo "==> Building WordPress test container..." docker build -t test-wordpress:latest \ crates/integration-tests/fixtures/frameworks/wordpress/ @@ -69,6 +76,7 @@ docker build \ echo "==> Running integration tests (target: $TARGET, origin port: $ORIGIN_PORT)..." WASM_BINARY_PATH="$REPO_ROOT/target/wasm32-wasip1/release/trusted-server-adapter-fastly.wasm" \ +AXUM_BINARY_PATH="$REPO_ROOT/target/debug/trusted-server-axum" \ INTEGRATION_ORIGIN_PORT="$ORIGIN_PORT" \ RUST_LOG=info \ cargo test \