diff --git a/.cargo/config.toml.local b/.cargo/config.toml.local index 767884b..7bec491 100644 --- a/.cargo/config.toml.local +++ b/.cargo/config.toml.local @@ -8,6 +8,7 @@ git-fetch-with-cli = true edgezero-adapter-axum = { path = "../edgezero/crates/edgezero-adapter-axum" } edgezero-adapter-cloudflare = { path = "../edgezero/crates/edgezero-adapter-cloudflare" } edgezero-adapter-fastly = { path = "../edgezero/crates/edgezero-adapter-fastly" } +edgezero-adapter-spin = { path = "../edgezero/crates/edgezero-adapter-spin" } edgezero-cli = { path = "../edgezero/crates/edgezero-cli" } edgezero-core = { path = "../edgezero/crates/edgezero-core" } edgezero-macros = { path = "../edgezero/crates/edgezero-macros" } diff --git a/.claude/agents/build-validator.md b/.claude/agents/build-validator.md index 6d95a02..570125f 100644 --- a/.claude/agents/build-validator.md +++ b/.claude/agents/build-validator.md @@ -15,6 +15,7 @@ cargo build --workspace --all-targets --all-features ``` cargo build -p mocktioneer-adapter-fastly --features fastly --target wasm32-wasip1 cargo build -p mocktioneer-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown +cargo build --release -p mocktioneer-adapter-spin --features spin --target wasm32-wasip2 ``` ## Feature matrix diff --git a/.claude/agents/verify-app.md b/.claude/agents/verify-app.md index eab52ac..524c588 100644 --- a/.claude/agents/verify-app.md +++ b/.claude/agents/verify-app.md @@ -26,6 +26,7 @@ Zero warnings required. Report any clippy lints or format violations. ``` cargo build -p mocktioneer-adapter-fastly --features fastly --target wasm32-wasip1 cargo build -p mocktioneer-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown +cargo build --release -p mocktioneer-adapter-spin --features spin --target wasm32-wasip2 ``` Both WASM targets must compile. Report any errors with the exact compiler output. diff --git a/.claude/commands/check-ci.md b/.claude/commands/check-ci.md index 918c0ea..b9c256e 100644 --- a/.claude/commands/check-ci.md +++ b/.claude/commands/check-ci.md @@ -4,5 +4,6 @@ Run the full CI gate checks locally before pushing. Run all commands sequentiall 2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` 3. `cargo test --workspace --all-targets` 4. `cargo check --workspace --all-targets --features "fastly cloudflare"` +5. `cargo run -p mocktioneer-cli -- config validate --strict --app-config mocktioneer.toml.example` (`mocktioneer.toml` is gitignored; validate the committed template) If any step fails, show the errors and suggest fixes. Do not proceed to the next step until the current one passes. diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index c34d0cc..6aaa883 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -32,9 +32,9 @@ jobs: id: node-version run: | if [ -f .tool-versions ]; then - echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" else - echo "node-version=20" >> $GITHUB_OUTPUT + echo "node-version=20" >> "$GITHUB_OUTPUT" fi - name: Setup Node.js diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index cffa3e7..7b061a3 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -44,11 +44,11 @@ jobs: - name: Get Rust version from .tool-versions id: rust - run: echo "version=$(grep '^rust' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "version=$(grep '^rust' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" - name: Get short SHA id: sha - run: echo "short=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" - name: Log in to GHCR uses: docker/login-action@v3 @@ -77,7 +77,44 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build and push + # Build once and load locally (no registry push yet) so the smoke test + # can gate publication — a broken image must never reach its final tag. + # `cache-to` is populated here so the publish step below is a near-instant + # cache hit of the exact same image. + - name: Build and load for smoke test + uses: docker/build-push-action@v6 + with: + context: . + load: true + push: false + tags: mocktioneer:ci-smoke + platforms: linux/amd64 + build-args: RUST_VERSION=${{ steps.rust.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Smoke test — reachable on mapped port and serves bids + run: | + cid="$(docker run -d -p 8787:8787 mocktioneer:ci-smoke)" + # Wait for the mapped host port — proves the container binds 0.0.0.0, + # not 127.0.0.1 (a loopback bind would make this fail). + for _ in $(seq 1 30); do + curl -fsS http://127.0.0.1:8787/ >/dev/null 2>&1 && break + sleep 2 + done + curl -fsS http://127.0.0.1:8787/ >/dev/null + # The baked-in config blob should let the auction endpoint serve a bid. + code="$(curl -s -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:8787/openrtb2/auction \ + -H 'content-type: application/json' \ + -d '{"id":"t","imp":[{"id":"1","banner":{"w":300,"h":250}}]}')" + echo "auction status: $code" + docker logs "$cid" 2>&1 | tail -20 || true + docker rm -f "$cid" >/dev/null 2>&1 || true + test "$code" = "200" + + # Only now publish — reuses the gha cache from the build above, so this + # pushes the exact image the smoke test just validated (not a rebuild). + - name: Push validated image uses: docker/build-push-action@v6 with: context: . @@ -87,4 +124,3 @@ jobs: platforms: linux/amd64 build-args: RUST_VERSION=${{ steps.rust.outputs.version }} cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 6360c63..8047458 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -34,7 +34,7 @@ jobs: - name: Retrieve Rust version id: rust-version - run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up rust toolchain @@ -64,7 +64,7 @@ jobs: - name: Retrieve Node.js version id: node-version working-directory: . - run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "node-version=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Use Node.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bee130..85a5a90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: - name: Retrieve Rust version id: rust-version - run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up Rust tool chain @@ -52,6 +52,24 @@ jobs: - name: Check feature compilation run: cargo check --workspace --all-targets --features "fastly cloudflare" + - name: Validate typed app config + run: | + # `mocktioneer.toml` is gitignored (per-env); the committed template is + # `mocktioneer.toml.example`. Materialise it so `config validate` (which + # reads the `.toml` default path) has a file to check. + cp mocktioneer.toml.example mocktioneer.toml + cargo run -p mocktioneer-cli -- config validate --strict + + - name: Seed a non-default cpm and assert it round-trips (axum) + run: | + printf 'bid_cpm = 0.35\n' > "${RUNNER_TEMP}/seed.toml" + cargo run -p mocktioneer-cli -- config push --adapter axum --yes --app-config "${RUNNER_TEMP}/seed.toml" + # edgezero #269 stores the typed config as a single blob envelope under + # the store key; bid_cpm lives at ` | fromjson | .data.bid_cpm`. + got="$(jq -r '.mocktioneer_config | fromjson | .data.bid_cpm' .edgezero/local-config-mocktioneer_config.json)" + test "$got" = "0.35" + rm -f .edgezero/local-config-mocktioneer_config.json + adapter-wasm-tests: name: ${{ matrix.adapter }} wasm tests runs-on: ubuntu-latest @@ -68,8 +86,8 @@ jobs: runner_env: CARGO_TARGET_WASM32_WASIP1_RUNNER runner_value: viceroy run - adapter: spin - target: wasm32-wasip1 - runner_env: CARGO_TARGET_WASM32_WASIP1_RUNNER + target: wasm32-wasip2 + runner_env: CARGO_TARGET_WASM32_WASIP2_RUNNER runner_value: wasmtime run steps: - uses: actions/checkout@v6 @@ -89,7 +107,7 @@ jobs: - name: Retrieve Rust version id: rust-version - run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + run: echo "rust-version=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up Rust tool chain @@ -183,8 +201,8 @@ jobs: - name: Retrieve tool versions id: tool-versions run: | - echo "rust=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT - echo "node=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> $GITHUB_OUTPUT + echo "rust=$(grep '^rust ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" + echo "node=$(grep '^nodejs ' .tool-versions | awk '{print $2}')" >> "$GITHUB_OUTPUT" shell: bash - name: Set up Rust tool chain diff --git a/.gitignore b/.gitignore index 711cde0..20304b4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,14 @@ node_modules/ playwright-report/ test-results/ -# Cloudflare Workers -.wrangler/ +# EdgeZero local config/kv state (config push --adapter axum, etc.) +.edgezero/ +# Cloudflare +.wrangler/ # Spin .spin/ + +# Mocktioneer +mocktioneer.toml diff --git a/CLAUDE.md b/CLAUDE.md index f2af53c..fa31608 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ Mocktioneer is a deterministic OpenRTB banner bidder for edge platforms. It lets you test client integrations (Prebid.js, Prebid Server, custom SDKs) without depending on third-party bidders or origin backends. Write once, deploy to Fastly Compute, Cloudflare Workers, or native Axum servers. The codebase is a -Cargo workspace with 4 crates under `crates/`, a VitePress documentation site +Cargo workspace with 6 crates under `crates/`, a VitePress documentation site under `docs/`, Playwright e2e tests under `tests/playwright/`, and CI workflows under `.github/workflows/`. @@ -18,7 +18,8 @@ crates/ mocktioneer-adapter-axum/ # Native Axum HTTP server mocktioneer-adapter-cloudflare/ # Cloudflare Workers bridge (wasm32-unknown-unknown) mocktioneer-adapter-fastly/ # Fastly Compute bridge (wasm32-wasip1) - mocktioneer-adapter-spin/ # Spin / Fermyon bridge (wasm32-wasip1) + mocktioneer-adapter-spin/ # Spin / Fermyon bridge (wasm32-wasip2) + mocktioneer-cli/ # Custom CLI: edgezero commands + typed config validate/push docs/ # VitePress documentation site (Node.js) examples/ # curl/shell scripts for endpoint demos tests/playwright/ # Playwright e2e tests (creative visibility, sizes) @@ -52,7 +53,12 @@ cargo run -p mocktioneer-adapter-axum # Run via EdgeZero CLI edgezero-cli serve --adapter cloudflare # Cloudflare on :8787 edgezero-cli serve --adapter fastly # Fastly on :7676 -edgezero-cli serve --adapter spin # Spin on :3000 +edgezero-cli serve --adapter spin # Spin on :3000 — NOTE: `spin up` is + # currently blocked (spin-sdk 6.0.0 + # imports wasi:http@0.3.0-rc, which no + # released Spin runtime provides). + # Build + wasmtime contract tests pass; + # live serve needs an upstream fix. # Playwright e2e tests cd tests/playwright && npm test @@ -70,7 +76,7 @@ faster iteration since nearly all business logic lives there. | ---------- | ------------------------ | ---------------------------------------------------- | | Fastly | `wasm32-wasip1` | Requires Viceroy for local testing | | Cloudflare | `wasm32-unknown-unknown` | Requires `wrangler` for dev/deploy | -| Spin | `wasm32-wasip1` | Requires `spin` for dev/deploy; tests via `wasmtime` | +| Spin | `wasm32-wasip2` | Requires `spin` for dev/deploy; tests via `wasmtime` | | Axum | Native (host triple) | Standard Tokio runtime | ## Coding Conventions @@ -154,7 +160,12 @@ through `render.rs`. Do not inline ad markup in handlers. ## Key Constants -- `FIXED_BID_CPM: f64 = 0.20` — fixed price for all Mocktioneer-generated bids +- `FIXED_BID_CPM: f64 = 0.20` — the auction/APS builders' default `cpm` + argument and the shipped value in `mocktioneer.toml`. At runtime the + OpenRTB/APS handlers read `bid_cpm` from the typed `MocktioneerConfig` blob via + the fail-loud `AppConfig` extractor (edgezero #269 blob model), so a deploy + must `config push` once before those endpoints serve — it is **not** a runtime + fallback - `STANDARD_SIZES` — 13 standard IAB sizes as a const array (300x250, 728x90, 320x50, etc.) ## CI Gates @@ -165,8 +176,10 @@ Every PR must pass: 2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` 3. `cargo test --workspace --all-targets` 4. `cargo check --workspace --all-targets --features "fastly cloudflare"` -5. Playwright e2e tests (`tests/playwright/`) -6. ESLint + Prettier on `docs/` +5. `cargo run -p mocktioneer-cli -- config validate --strict --app-config mocktioneer.toml.example` + (`mocktioneer.toml` is gitignored; CI copies the template first — see `test.yml`) +6. Playwright e2e tests (`tests/playwright/`) +7. ESLint + Prettier on `docs/` Docker image is built and pushed to `ghcr.io/stackpop/mocktioneer` on push to main and on releases. diff --git a/Cargo.lock b/Cargo.lock index f6e1af6..6ec0674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -25,9 +37,9 @@ checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" dependencies = [ "alloc-no-stdlib", ] @@ -41,11 +53,61 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "async-compression" @@ -78,7 +140,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -89,7 +151,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -106,9 +168,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" dependencies = [ "aws-lc-sys", "zeroize", @@ -116,14 +178,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.41.0" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -198,9 +261,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -222,9 +285,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.3" +version = "8.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -233,9 +296,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.1" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -249,9 +312,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cast" @@ -261,9 +324,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.63" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -296,6 +359,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -305,6 +408,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "colored" version = "3.1.1" @@ -392,6 +501,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -416,7 +535,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -440,7 +559,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -451,7 +570,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -470,7 +589,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] @@ -492,7 +610,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -502,7 +620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -532,7 +650,7 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -571,15 +689,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "edgezero-adapter" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" +dependencies = [ + "toml", +] + [[package]] name = "edgezero-adapter-axum" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "anyhow", "async-trait", "axum", "bytes", + "ctor", + "edgezero-adapter", "edgezero-core", "futures", "futures-util", @@ -587,35 +715,43 @@ dependencies = [ "log", "redb", "reqwest", + "serde_json", "simple_logger", "thiserror 2.0.18", "tokio", + "toml", "tower", "tracing", + "walkdir", ] [[package]] name = "edgezero-adapter-cloudflare" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "anyhow", "async-trait", "brotli", "bytes", + "ctor", + "edgezero-adapter", "edgezero-core", "flate2", "futures", "futures-util", "log", "serde_json", + "tempfile", + "toml_edit", + "walkdir", "worker", ] [[package]] name = "edgezero-adapter-fastly" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "anyhow", "async-stream", @@ -623,6 +759,8 @@ dependencies = [ "brotli", "bytes", "chrono", + "ctor", + "edgezero-adapter", "edgezero-core", "fastly", "fern", @@ -631,30 +769,70 @@ dependencies = [ "futures-util", "log", "log-fastly", + "serde", + "serde_json", + "sha2 0.10.9", "thiserror 2.0.18", + "toml_edit", + "walkdir", ] [[package]] name = "edgezero-adapter-spin" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "anyhow", "async-trait", "brotli", "bytes", + "ctor", + "edgezero-adapter", "edgezero-core", "flate2", "futures", "futures-util", "log", + "rusqlite", + "serde", + "serde_json", "spin-sdk", + "subtle", + "thiserror 2.0.18", + "toml", + "toml_edit", + "walkdir", +] + +[[package]] +name = "edgezero-cli" +version = "0.1.0" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" +dependencies = [ + "chrono", + "clap", + "edgezero-adapter", + "edgezero-adapter-axum", + "edgezero-adapter-cloudflare", + "edgezero-adapter-fastly", + "edgezero-adapter-spin", + "edgezero-core", + "futures", + "handlebars", + "log", + "serde", + "serde_json", + "similar", + "simple_logger", + "thiserror 2.0.18", + "toml", + "validator", ] [[package]] name = "edgezero-core" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "anyhow", "async-compression", @@ -668,9 +846,12 @@ dependencies = [ "http-body", "log", "matchit 0.9.2", + "ryu", "serde", "serde_json", + "serde_path_to_error", "serde_urlencoded", + "sha2 0.10.9", "thiserror 2.0.18", "toml", "tower-service", @@ -682,13 +863,14 @@ dependencies = [ [[package]] name = "edgezero-macros" version = "0.1.0" -source = "git+https://github.com/stackpop/edgezero.git?branch=main#7ec2ad1de50477536854c2dc96acb2699b7d0026" +source = "git+https://github.com/stackpop/edgezero.git?tag=v0.0.4#9e661ae520a8130660f18fd10f42703d7f3e050b" dependencies = [ "log", "proc-macro2", "quote", "serde", - "syn 2.0.117", + "serde_json", + "syn 2.0.118", "toml", "validator", ] @@ -725,6 +907,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastly" version = "0.12.1" @@ -787,6 +981,12 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "fern" version = "0.7.1" @@ -826,9 +1026,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "foldhash" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "form_urlencoded" @@ -901,7 +1101,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -952,7 +1152,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -972,22 +1172,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", ] [[package]] name = "handlebars" -version = "6.4.1" +version = "6.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d43ccdfe15a81ab0a8af639e90254227c9a46afd9c5f5b6ec7efaa345c4b0f00" +checksum = "f26569a2763497b7bd3fbd19374b774ea6038c5293678771259cd534d49740ff" dependencies = [ "derive_builder", "log", @@ -1001,11 +1199,11 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "foldhash", + "ahash", ] [[package]] @@ -1013,6 +1211,18 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] [[package]] name = "heck" @@ -1022,9 +1232,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1281,6 +1491,12 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1323,7 +1539,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1342,7 +1558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1391,6 +1607,35 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-section" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24670b639492630905459a6c7d47f063d33c2d4fcd5362f6e5827c5613976c9f" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b0a3383c2a1002d11349c92c85a666a5fb679e96c79d782cf0dbe557fd6ee" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -1399,9 +1644,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" dependencies = [ "serde_core", ] @@ -1443,9 +1688,9 @@ checksum = "8863b587001c1b9a8a4e36008cebc6b3612cb1226fe2de94858e06092687b608" [[package]] name = "memchr" -version = "2.8.1" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "mime" @@ -1480,7 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -1503,9 +1748,11 @@ dependencies = [ name = "mocktioneer-adapter-cloudflare" version = "0.1.0" dependencies = [ + "async-trait", "edgezero-adapter-cloudflare", "edgezero-core", "mocktioneer-core", + "serde_json", "wasm-bindgen-test", "worker", ] @@ -1514,11 +1761,13 @@ dependencies = [ name = "mocktioneer-adapter-fastly" version = "0.1.0" dependencies = [ + "async-trait", "edgezero-adapter-fastly", "edgezero-core", "fastly", "log", "mocktioneer-core", + "serde_json", ] [[package]] @@ -1533,10 +1782,21 @@ dependencies = [ "spin-sdk", ] +[[package]] +name = "mocktioneer-cli" +version = "0.1.0" +dependencies = [ + "clap", + "edgezero-cli", + "log", + "mocktioneer-core", +] + [[package]] name = "mocktioneer-core" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "base64", "ed25519-dalek", @@ -1574,9 +1834,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-modular" -version = "0.6.1" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" +checksum = "fc41a1374056e9672221567958a66c16be12d0e2c1b408761e14d901c237d5e0" [[package]] name = "num-order" @@ -1612,6 +1872,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -1666,7 +1932,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1696,7 +1962,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1715,6 +1981,12 @@ dependencies = [ "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "potential_utf" version = "0.1.5" @@ -1746,7 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1768,7 +2040,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1782,9 +2054,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1802,9 +2074,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "aws-lc-rs", "bytes", @@ -1838,9 +2110,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1906,9 +2178,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1929,9 +2201,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -1941,7 +2213,9 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", + "futures-channel", "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -1956,6 +2230,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls", @@ -1983,20 +2259,24 @@ dependencies = [ ] [[package]] -name = "routefinder" -version = "0.5.4" +name = "rusqlite" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ - "smartcow", - "smartstring", + "bitflags 2.13.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", ] [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -2007,11 +2287,24 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "aws-lc-rs", "once_cell", @@ -2035,9 +2328,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ "web-time", "zeroize", @@ -2118,7 +2411,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.12.1", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -2179,7 +2472,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2188,6 +2481,7 @@ version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ + "indexmap", "itoa", "memchr", "serde", @@ -2214,7 +2508,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2309,6 +2603,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "simple_logger" version = "5.2.0" @@ -2329,29 +2629,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "smartcow" -version = "0.2.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" -dependencies = [ - "smartstring", -] - -[[package]] -name = "smartstring" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" -dependencies = [ - "autocfg", - "static_assertions", - "version_check", -] +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" @@ -2363,25 +2643,12 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spin-executor" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bba409d00af758cd5de128da4a801e891af0545138f66a688f025f6d4e33870b" -dependencies = [ - "futures", - "once_cell", - "wasi 0.13.1+wasi-0.2.0", -] - [[package]] name = "spin-macro" -version = "5.2.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f959f16928e3c023468e41da9ebb77442e2ce22315e8dab11508fe76b3567ee1" +checksum = "11e483b94d5bcfac493caf0427fa875063e3e8604d0466a4ab491ec200a42857" dependencies = [ - "anyhow", - "bytes", "proc-macro2", "quote", "syn 1.0.109", @@ -2389,24 +2656,19 @@ dependencies = [ [[package]] name = "spin-sdk" -version = "5.2.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8951c7c4ab7f87f332d497789eeed9631c8116988b628b4851eb2fa999ead019" +checksum = "4fd2abac3eb2ee249c2241ab87f7b1287f36172c8cc1ea815c19c85e41ede44d" dependencies = [ "anyhow", - "async-trait", "bytes", - "chrono", - "form_urlencoded", "futures", "http", - "once_cell", - "routefinder", - "spin-executor", + "http-body", + "http-body-util", "spin-macro", "thiserror 2.0.18", - "wasi 0.13.1+wasi-0.2.0", - "wit-bindgen 0.51.0", + "wasip3", ] [[package]] @@ -2425,12 +2687,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -2455,7 +2711,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2477,9 +2733,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2503,7 +2759,20 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] @@ -2532,7 +2801,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2543,17 +2812,16 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "time" -version = "0.3.47" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -2565,15 +2833,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" dependencies = [ "num-conv", "time-core", @@ -2628,7 +2896,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2650,10 +2918,19 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", ] [[package]] @@ -2665,13 +2942,26 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] @@ -2702,7 +2992,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.12.1", + "bitflags 2.13.0", "bytes", "futures-util", "http", @@ -2746,7 +3036,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2812,13 +3102,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -2850,9 +3146,15 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2884,31 +3186,26 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.13.1+wasi-0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f43d1c36145feb89a3e61aa0ba3e582d976a8ab77f1474aa0adb80800fe0cf8" -dependencies = [ - "wit-bindgen-rt", -] - [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "0.6.0+wasi-0.3.0-rc-2026-03-15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "ed83456dd6a0b8581998c0365e4651fa2997e5093b49243b7f35391afaa7a3d9" dependencies = [ - "wit-bindgen 0.51.0", + "bytes", + "http", + "http-body", + "thiserror 2.0.18", + "wit-bindgen 0.57.1", ] [[package]] @@ -2953,7 +3250,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -2996,7 +3293,7 @@ checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3007,9 +3304,9 @@ checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" dependencies = [ "leb128fmt", "wasmparser", @@ -3017,9 +3314,9 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.244.0" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "665fe59e56cc9b419ca6fcca56673e3421d1a5011e3b65caf6b726fd9e041d10" dependencies = [ "anyhow", "indexmap", @@ -3042,12 +3339,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" dependencies = [ - "bitflags 2.12.1", - "hashbrown 0.15.5", + "bitflags 2.13.0", + "hashbrown 0.17.1", "indexmap", "semver", ] @@ -3074,9 +3371,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" dependencies = [ "rustls-pki-types", ] @@ -3111,7 +3408,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3122,7 +3419,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3305,6 +3602,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.3" @@ -3317,8 +3623,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ - "bitflags 2.12.1", - "wit-bindgen-rust-macro", + "bitflags 2.13.0", ] [[package]] @@ -3327,40 +3632,33 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags 2.12.1", + "bitflags 2.13.0", + "futures", + "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +checksum = "02dee27a2dc20d1008016c742ec9fc6ea498492994ba3750be7454cbc97ff04c" dependencies = [ "anyhow", "heck", "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0780cf7046630ed70f689a098cd8d56c5c3b22f2a7379bbdb088879963ff96" -dependencies = [ - "bitflags 2.12.1", -] - [[package]] name = "wit-bindgen-rust" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +checksum = "b5007dae772945b7a5003d69d90a3a4a78929d41f19d004e980c4259a6af4484" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", - "syn 2.0.117", + "syn 2.0.118", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -3368,27 +3666,27 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +checksum = "af9237d678e3513ad24e96fe98beacdc0db6405284ba2a2400418cf0d42caa89" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" -version = "0.244.0" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +checksum = "9d567162a6b9843080e5e0053f696623ff694bae8ae017c9ec536d1873bbe3d8" dependencies = [ "anyhow", - "bitflags 2.12.1", + "bitflags 2.13.0", "indexmap", "log", "serde", @@ -3402,11 +3700,12 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.244.0" +version = "0.247.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +checksum = "8ffe4064318cdf3c08cb99343b44c039fcefe61ccdf58aa9975285f13d74d1fc" dependencies = [ "anyhow", + "hashbrown 0.17.1", "id-arena", "indexmap", "log", @@ -3459,7 +3758,7 @@ dependencies = [ "proc-macro2", "quote", "strum", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-macro-support", @@ -3503,28 +3802,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3544,15 +3843,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" @@ -3584,7 +3883,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bb4c1f8..a0a144c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/mocktioneer-adapter-cloudflare", "crates/mocktioneer-adapter-fastly", "crates/mocktioneer-adapter-spin", + "crates/mocktioneer-cli", ] resolver = "2" @@ -21,13 +22,14 @@ anyhow = "1" async-trait = "0.1" axum = "0.8" base64 = "0.22" +clap = { version = "4", features = ["derive"] } ed25519-dalek = "2.1" -edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-axum", default-features = false } -edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-cloudflare", default-features = false } -edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-fastly", default-features = false } -edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-adapter-spin", default-features = false } -edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-cli" } -edgezero-core = { git = "https://github.com/stackpop/edgezero.git", branch = "main", package = "edgezero-core" } +edgezero-adapter-axum = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-adapter-axum", default-features = false } +edgezero-adapter-cloudflare = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-adapter-cloudflare", default-features = false } +edgezero-adapter-fastly = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-adapter-fastly", default-features = false } +edgezero-adapter-spin = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-adapter-spin", default-features = false } +edgezero-cli = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-cli" } +edgezero-core = { git = "https://github.com/stackpop/edgezero.git", tag = "v0.0.4", package = "edgezero-core" } fastly = "0.12.1" futures = { version = "0.3", features = ["std", "executor"] } futures-util = "0.3.32" @@ -40,7 +42,7 @@ serde_json = "1" serde_repr = "0.1" sha2 = "0.10" simple_logger = "5" -spin-sdk = { version = "5.2", default-features = false } +spin-sdk = { version = "~6.0", default-features = false, features = ["http", "key-value", "variables"] } subtle = "2" thiserror = "1.0" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/Dockerfile b/Dockerfile index 7a4847f..3459e9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,14 +17,32 @@ COPY crates/mocktioneer-core/Cargo.toml crates/mocktioneer-core/Cargo.toml COPY crates/mocktioneer-adapter-axum/Cargo.toml crates/mocktioneer-adapter-axum/Cargo.toml COPY crates/mocktioneer-adapter-cloudflare/Cargo.toml crates/mocktioneer-adapter-cloudflare/Cargo.toml COPY crates/mocktioneer-adapter-fastly/Cargo.toml crates/mocktioneer-adapter-fastly/Cargo.toml +COPY crates/mocktioneer-adapter-spin/Cargo.toml crates/mocktioneer-adapter-spin/Cargo.toml +COPY crates/mocktioneer-cli/Cargo.toml crates/mocktioneer-cli/Cargo.toml COPY crates ./crates COPY edgezero.toml ./edgezero.toml RUN cargo fetch --locked -RUN cargo build --locked --release -p mocktioneer-adapter-axum +RUN cargo build --locked --release -p mocktioneer-adapter-axum -p mocktioneer-cli -FROM debian:stable-slim AS runtime +# `mocktioneer.toml` is gitignored (per-env); ship the committed template as the +# config the image seeds from. Copied here (after the build layers) so that +# editing `bid_cpm` in the template doesn't invalidate the dependency-fetch and +# compile caches — only the cheap `config push` layer below re-runs. +COPY mocktioneer.toml.example ./mocktioneer.toml + +# Seed the default typed-config blob into `.edgezero/` so the OpenRTB/APS +# endpoints serve out-of-the-box: under edgezero #269 they read `bid_cpm` +# through the fail-loud `AppConfig` extractor and 503 until a blob is pushed. +# (Override at deploy time by mounting your own +# `/app/.edgezero/local-config-mocktioneer_config.json`.) +RUN ./target/release/mocktioneer-cli config push --adapter axum --yes + +# Pin the runtime base to the same Debian release as the builder +# (`rust:1.95.0-slim-bookworm`) so the runtime glibc can't drift out from under +# the build env on a later rebuild. +FROM debian:bookworm-slim AS runtime RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ @@ -33,10 +51,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ RUN useradd --create-home --uid 10001 appuser +WORKDIR /app + COPY --from=builder /app/target/release/mocktioneer-adapter-axum /usr/local/bin/mocktioneer-adapter-axum +# The Axum config store reads `./.edgezero/local-config-.json` relative to +# the working directory, so the seeded blob must sit under the runtime WORKDIR. +COPY --from=builder --chown=10001:10001 /app/.edgezero /app/.edgezero USER appuser +# Bind to all interfaces so `docker run -p :8787` (and k8s) can reach the +# service — EdgeZero's Axum dev server otherwise defaults to 127.0.0.1:8787, +# which is only reachable from inside the container. +ENV EDGEZERO__ADAPTER__HOST=0.0.0.0 \ + EDGEZERO__ADAPTER__PORT=8787 + EXPOSE 8787 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ diff --git a/README.md b/README.md index 242c6a8..f636935 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,18 @@ Deterministic OpenRTB banner bidder for edge platforms. Test client integrations ## Quick Start ```bash -# Clone and run locally +# Clone git clone https://github.com/stackpop/mocktioneer.git cd mocktioneer + +# Create your local app config from the template (mocktioneer.toml is gitignored) +cp mocktioneer.toml.example mocktioneer.toml + +# Seed the typed app config once (the auction/APS endpoints read bid_cpm from it +# via the fail-loud AppConfig extractor and error until it is pushed). +cargo run -p mocktioneer-cli -- config push --adapter axum --yes + +# Run locally cargo run -p mocktioneer-adapter-axum # Test the auction endpoint @@ -40,6 +49,7 @@ Full documentation is available at **[stackpop.github.io/mocktioneer](https://st | `POST /e/dtb/bid` | APS TAM bid request | | `GET /static/creatives/{size}.html` | Creative wrapper | | `GET /_/sizes` | Supported sizes with pricing | +| `GET /_mocktioneer/{manifest,config,routes}` | Introspection (manifest / config / route table) | See the [full API reference](https://stackpop.github.io/mocktioneer/api/) for all endpoints. @@ -48,8 +58,16 @@ See the [full API reference](https://stackpop.github.io/mocktioneer/api/) for al ```bash cargo test # Run tests cargo run -p mocktioneer-adapter-axum # Local server (Axum) on :8787 -edgezero-cli serve --adapter cloudflare # Local server (Cloudflare) on :8787 -edgezero-cli serve --adapter fastly # Local server (Fastly) on :7676 + +# Serve other adapters via the in-repo CLI (or the external edgezero-cli): +cargo run -p mocktioneer-cli -- serve --adapter cloudflare # Cloudflare on :8787 +cargo run -p mocktioneer-cli -- serve --adapter fastly # Fastly on :7676 + +# Typed app config (validate / diff / push bid_cpm as a blob envelope). +# Auction + APS endpoints require a one-time `config push` per deploy. +cargo run -p mocktioneer-cli -- config validate --strict +cargo run -p mocktioneer-cli -- config diff --adapter axum +cargo run -p mocktioneer-cli -- config push --adapter axum --yes ``` ## License diff --git a/crates/mocktioneer-adapter-axum/src/main.rs b/crates/mocktioneer-adapter-axum/src/main.rs index f85b0d5..b094cd7 100644 --- a/crates/mocktioneer-adapter-axum/src/main.rs +++ b/crates/mocktioneer-adapter-axum/src/main.rs @@ -4,7 +4,7 @@ use edgezero_adapter_axum::dev_server::run_app; use mocktioneer_core::MocktioneerApp; fn main() { - if let Err(err) = run_app::(include_str!("../../../edgezero.toml")) { + if let Err(err) = run_app::() { #[expect( clippy::print_stderr, reason = "startup-error path: logger may not be initialised yet" diff --git a/crates/mocktioneer-adapter-cloudflare/Cargo.toml b/crates/mocktioneer-adapter-cloudflare/Cargo.toml index ae422fb..91cec95 100644 --- a/crates/mocktioneer-adapter-cloudflare/Cargo.toml +++ b/crates/mocktioneer-adapter-cloudflare/Cargo.toml @@ -29,6 +29,9 @@ worker = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "=0.3.71" +# Contract tests seed the typed-config blob in-process (no miniflare/KV fixture). +async-trait = { workspace = true } +serde_json = { workspace = true } [lints] workspace = true diff --git a/crates/mocktioneer-adapter-cloudflare/src/lib.rs b/crates/mocktioneer-adapter-cloudflare/src/lib.rs index 76e1a8e..9dc67fa 100644 --- a/crates/mocktioneer-adapter-cloudflare/src/lib.rs +++ b/crates/mocktioneer-adapter-cloudflare/src/lib.rs @@ -8,11 +8,5 @@ use worker::*; #[cfg(target_arch = "wasm32")] #[event(fetch)] pub async fn main(req: Request, env: Env, ctx: Context) -> Result { - edgezero_adapter_cloudflare::run_app::( - include_str!("../../../edgezero.toml"), - req, - env, - ctx, - ) - .await + edgezero_adapter_cloudflare::run_app::(req, env, ctx).await } diff --git a/crates/mocktioneer-adapter-cloudflare/tests/contract.rs b/crates/mocktioneer-adapter-cloudflare/tests/contract.rs index f6f709b..cc75efb 100644 --- a/crates/mocktioneer-adapter-cloudflare/tests/contract.rs +++ b/crates/mocktioneer-adapter-cloudflare/tests/contract.rs @@ -1,15 +1,38 @@ #![cfg(all(feature = "cloudflare", target_arch = "wasm32"))] -#![expect( - deprecated, - reason = "exercise the low-level dispatch path while it remains public" -)] -use edgezero_adapter_cloudflare::request::dispatch; +use async_trait::async_trait; +use edgezero_adapter_cloudflare::request::CloudflareService; +use edgezero_core::blob_envelope::BlobEnvelope; +use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; use mocktioneer_core::build_app; +use std::collections::HashMap; +use std::sync::Arc; use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure}; -use worker::wasm_bindgen::JsCast as _; +use worker::wasm_bindgen::{JsCast as _, JsValue}; use worker::{Context, Env, Method as CfMethod, Request as CfRequest, RequestInit}; +/// In-memory `ConfigStore` so the auction path can be seeded in-process, with no +/// miniflare/workerd KV fixture. Mirrors the core-crate test double. +struct MapConfigStore(HashMap); + +#[async_trait(?Send)] +impl ConfigStore for MapConfigStore { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + Ok(self.0.get(key).cloned()) + } +} + +/// A config handle holding the typed-config blob for `{ "bid_cpm": }`. +/// An injected handle is bound under default_key `"default"` by the Cloudflare +/// service builder (`synthesise_store_registries`), so the blob lives there. +fn seeded_config_handle(bid_cpm: f64) -> ConfigStoreHandle { + let data = serde_json::json!({ "bid_cpm": bid_cpm }); + let blob = serde_json::to_string(&BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_owned())) + .expect("serialize envelope"); + let store = MapConfigStore([("default".to_owned(), blob)].into_iter().collect()); + ConfigStoreHandle::new(Arc::new(store)) +} + // `run_in_browser` selects the wasm-bindgen browser harness. In CI this runs // headless in Firefox (via the geckodriver that ships on the ubuntu-latest // runners); locally it picks up whichever browser driver is on PATH. @@ -41,7 +64,10 @@ async fn root_dispatches_through_cloudflare_adapter() { let req = cf_request(CfMethod::Get, "/"); let (env, ctx) = test_env_ctx(); - let mut response = dispatch(&app, req, env, ctx).await.expect("cf response"); + let mut response = CloudflareService::new(&app) + .dispatch(req, env, ctx) + .await + .expect("cf response"); assert_eq!(response.status_code(), 200); let body = response.bytes().await.expect("body bytes"); @@ -59,7 +85,10 @@ async fn pixel_returns_gif_through_cloudflare_adapter() { let req = cf_request(CfMethod::Get, "/pixel?pid=cloudflare-contract"); let (env, ctx) = test_env_ctx(); - let response = dispatch(&app, req, env, ctx).await.expect("cf response"); + let response = CloudflareService::new(&app) + .dispatch(req, env, ctx) + .await + .expect("cf response"); assert_eq!(response.status_code(), 200); let content_type = response @@ -69,3 +98,50 @@ async fn pixel_returns_gif_through_cloudflare_adapter() { .expect("content-type header"); assert_eq!(content_type, "image/gif"); } + +fn cf_post_json(path: &str, body: &str) -> CfRequest { + let mut init = RequestInit::new(); + init.with_method(CfMethod::Post); + + let headers = worker::Headers::new(); + headers.set("host", "test.local").expect("host header"); + headers + .set("content-type", "application/json") + .expect("content-type header"); + init.with_headers(headers); + init.with_body(Some(JsValue::from_str(body))); + + let url = format!("https://test.local{path}"); + CfRequest::new_with_init(&url, &init).expect("cf request") +} + +#[wasm_bindgen_test] +async fn auction_uses_seeded_cpm_through_cloudflare_adapter() { + let app = build_app(); + let body = serde_json::json!({ + "id": "rc", + "imp": [{ "id": "1", "banner": { "w": 300_i32, "h": 250_i32 } }] + }) + .to_string(); + let req = cf_post_json("/openrtb2/auction", &body); + let (env, ctx) = test_env_ctx(); + + // Full path through the adapter: request translation -> config-store bind -> + // `AppConfig` extractor -> auction handler -> response translation. + let mut response = CloudflareService::new(&app) + .with_config_handle(seeded_config_handle(0.35_f64)) + .dispatch(req, env, ctx) + .await + .expect("cf response"); + + assert_eq!(response.status_code(), 200); + let payload: serde_json::Value = + serde_json::from_slice(&response.bytes().await.expect("body bytes")).expect("json body"); + let price = payload["seatbid"][0]["bid"][0]["price"] + .as_f64() + .expect("bid price"); + assert!( + (price - 0.35_f64).abs() < f64::EPSILON, + "expected seeded cpm 0.35, got {price}", + ); +} diff --git a/crates/mocktioneer-adapter-fastly/Cargo.toml b/crates/mocktioneer-adapter-fastly/Cargo.toml index 6e7a74d..c0ee5a5 100644 --- a/crates/mocktioneer-adapter-fastly/Cargo.toml +++ b/crates/mocktioneer-adapter-fastly/Cargo.toml @@ -23,5 +23,10 @@ edgezero-adapter-fastly = { workspace = true, features = ["fastly"] } edgezero-core = { workspace = true } fastly = { workspace = true } +# Contract tests seed the typed-config blob in-process (no Viceroy fixture). +[target.'cfg(target_arch = "wasm32")'.dev-dependencies] +async-trait = { workspace = true } +serde_json = { workspace = true } + [lints] workspace = true diff --git a/crates/mocktioneer-adapter-fastly/src/main.rs b/crates/mocktioneer-adapter-fastly/src/main.rs index e966745..f1f0170 100644 --- a/crates/mocktioneer-adapter-fastly/src/main.rs +++ b/crates/mocktioneer-adapter-fastly/src/main.rs @@ -13,7 +13,7 @@ use mocktioneer_core::MocktioneerApp; #[cfg(target_arch = "wasm32")] #[fastly::main] pub fn main(req: Request) -> Result { - edgezero_adapter_fastly::run_app::(include_str!("../../../edgezero.toml"), req) + edgezero_adapter_fastly::run_app::(req) } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/mocktioneer-adapter-fastly/tests/contract.rs b/crates/mocktioneer-adapter-fastly/tests/contract.rs index ff34776..0e1eb60 100644 --- a/crates/mocktioneer-adapter-fastly/tests/contract.rs +++ b/crates/mocktioneer-adapter-fastly/tests/contract.rs @@ -1,13 +1,36 @@ #![cfg(all(feature = "fastly", target_arch = "wasm32"))] -#![expect( - deprecated, - reason = "exercise the low-level dispatch path while it remains public" -)] -use edgezero_adapter_fastly::request::dispatch; +use async_trait::async_trait; +use edgezero_adapter_fastly::request::FastlyService; +use edgezero_core::blob_envelope::BlobEnvelope; +use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; use fastly::http::{Method as FastlyMethod, StatusCode as FastlyStatus}; use fastly::Request as FastlyRequest; use mocktioneer_core::build_app; +use std::collections::HashMap; +use std::sync::Arc; + +/// In-memory `ConfigStore` so the auction path can be seeded in-process, with no +/// Viceroy config-store fixture. Mirrors the core-crate test double. +struct MapConfigStore(HashMap); + +#[async_trait(?Send)] +impl ConfigStore for MapConfigStore { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + Ok(self.0.get(key).cloned()) + } +} + +/// A config handle holding the typed-config blob for `{ "bid_cpm": }`. +/// An injected handle is bound under default_key `"default"` by the Fastly +/// service builder (`synthesise_store_registries`), so the blob lives there. +fn seeded_config_handle(bid_cpm: f64) -> ConfigStoreHandle { + let data = serde_json::json!({ "bid_cpm": bid_cpm }); + let blob = serde_json::to_string(&BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_owned())) + .expect("serialize envelope"); + let store = MapConfigStore([("default".to_owned(), blob)].into_iter().collect()); + ConfigStoreHandle::new(Arc::new(store)) +} fn fastly_request(method: FastlyMethod, path: &str) -> FastlyRequest { let mut req = FastlyRequest::new(method, format!("http://test.local{path}")); @@ -20,7 +43,9 @@ fn root_dispatches_through_fastly_adapter() { let app = build_app(); let req = fastly_request(FastlyMethod::GET, "/"); - let mut response = dispatch(&app, req).expect("fastly response"); + let mut response = FastlyService::new(&app) + .dispatch(req) + .expect("fastly response"); assert_eq!(response.get_status(), FastlyStatus::OK); let body = response.take_body_bytes(); @@ -37,7 +62,9 @@ fn pixel_returns_gif_through_fastly_adapter() { let app = build_app(); let req = fastly_request(FastlyMethod::GET, "/pixel?pid=fastly-contract"); - let response = dispatch(&app, req).expect("fastly response"); + let response = FastlyService::new(&app) + .dispatch(req) + .expect("fastly response"); assert_eq!(response.get_status(), FastlyStatus::OK); let content_type = response @@ -46,3 +73,34 @@ fn pixel_returns_gif_through_fastly_adapter() { .expect("content-type"); assert_eq!(content_type, "image/gif"); } + +#[test] +fn auction_uses_seeded_cpm_through_fastly_adapter() { + let app = build_app(); + let body = serde_json::json!({ + "id": "rc", + "imp": [{ "id": "1", "banner": { "w": 300_i32, "h": 250_i32 } }] + }) + .to_string(); + let req = fastly_request(FastlyMethod::POST, "/openrtb2/auction") + .with_header("content-type", "application/json") + .with_body(body); + + let mut response = FastlyService::new(&app) + .with_config_handle(seeded_config_handle(0.35_f64)) + .dispatch(req) + .expect("fastly response"); + + // Full path through the adapter: request translation -> config-store bind -> + // `AppConfig` extractor -> auction handler -> response translation. + assert_eq!(response.get_status(), FastlyStatus::OK); + let payload: serde_json::Value = + serde_json::from_slice(&response.take_body_bytes()).expect("json body"); + let price = payload["seatbid"][0]["bid"][0]["price"] + .as_f64() + .expect("bid price"); + assert!( + (price - 0.35_f64).abs() < f64::EPSILON, + "expected seeded cpm 0.35, got {price}", + ); +} diff --git a/crates/mocktioneer-adapter-spin/runtime-config.toml b/crates/mocktioneer-adapter-spin/runtime-config.toml new file mode 100644 index 0000000..90b66e7 --- /dev/null +++ b/crates/mocktioneer-adapter-spin/runtime-config.toml @@ -0,0 +1,5 @@ +# Spin runtime config: declares the KV labels the component may open. +# The `mocktioneer_config` label backs the EdgeZero `[stores.config]` +# store; the default SQLite backend persists to `.spin/sqlite_key_value.db`. +[key_value_store.mocktioneer_config] +type = "spin" diff --git a/crates/mocktioneer-adapter-spin/spin.toml b/crates/mocktioneer-adapter-spin/spin.toml index cec0d60..a606462 100644 --- a/crates/mocktioneer-adapter-spin/spin.toml +++ b/crates/mocktioneer-adapter-spin/spin.toml @@ -10,9 +10,10 @@ route = "/..." component = "mocktioneer" [component.mocktioneer] -source = "../../target/wasm32-wasip1/release/mocktioneer_adapter_spin.wasm" +source = "../../target/wasm32-wasip2/release/mocktioneer_adapter_spin.wasm" allowed_outbound_hosts = ["https://*:*"] +key_value_stores = ["mocktioneer_config"] [component.mocktioneer.build] -command = "cargo build --target wasm32-wasip1 --release" +command = "cargo build --target wasm32-wasip2 --release" watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/crates/mocktioneer-adapter-spin/src/lib.rs b/crates/mocktioneer-adapter-spin/src/lib.rs index e309ba0..d0cae24 100644 --- a/crates/mocktioneer-adapter-spin/src/lib.rs +++ b/crates/mocktioneer-adapter-spin/src/lib.rs @@ -1,22 +1,12 @@ -#![cfg_attr(target_arch = "wasm32", no_main)] -#![cfg_attr( - target_arch = "wasm32", - expect( - unsafe_code, - reason = "#[http_component] expands to WASI wit-bindgen code containing an export_name function plus unsafe declarations and blocks; this is the only entry into the Spin runtime" - ) -)] - #[cfg(target_arch = "wasm32")] use mocktioneer_core::MocktioneerApp; #[cfg(target_arch = "wasm32")] -use spin_sdk::http::{IncomingRequest, IntoResponse}; +use spin_sdk::http::{IntoResponse, Request}; #[cfg(target_arch = "wasm32")] -use spin_sdk::http_component; +use spin_sdk::http_service; #[cfg(target_arch = "wasm32")] -#[http_component] -async fn handle(req: IncomingRequest) -> anyhow::Result { - edgezero_adapter_spin::run_app::(include_str!("../../../edgezero.toml"), req) - .await +#[http_service] +async fn handle(req: Request) -> anyhow::Result { + edgezero_adapter_spin::run_app::(req).await } diff --git a/crates/mocktioneer-adapter-spin/src/main.rs b/crates/mocktioneer-adapter-spin/src/main.rs index 089a78d..f4f6e9d 100644 --- a/crates/mocktioneer-adapter-spin/src/main.rs +++ b/crates/mocktioneer-adapter-spin/src/main.rs @@ -1,7 +1,7 @@ #[expect( clippy::print_stderr, - reason = "host-side stub that exists solely to remind the operator to target wasm32-wasip1" + reason = "host-side stub that exists solely to remind the operator to target wasm32-wasip2" )] fn main() { - eprintln!("Run `spin up` or target wasm32-wasip1 to execute mocktioneer-adapter-spin."); + eprintln!("Run `spin up` or target wasm32-wasip2 to execute mocktioneer-adapter-spin."); } diff --git a/crates/mocktioneer-adapter-spin/tests/contract.rs b/crates/mocktioneer-adapter-spin/tests/contract.rs index 2b16c4a..063c396 100644 --- a/crates/mocktioneer-adapter-spin/tests/contract.rs +++ b/crates/mocktioneer-adapter-spin/tests/contract.rs @@ -1,10 +1,10 @@ //! Spin adapter contract tests. //! -//! Spin's `IncomingRequest` is a WASI handle that can only be constructed by +//! Spin's `Request` is a WASI handle that can only be constructed by //! the Spin runtime, so we cannot exercise the full `dispatch(req)` path the //! way the Fastly and Cloudflare contract tests do. Instead, these tests //! prove that `MocktioneerApp::build_app()` and `router().oneshot(...)` work -//! end-to-end under `wasm32-wasip1` via the `wasmtime` runner — the same +//! end-to-end under `wasm32-wasip2` via the `wasmtime` runner — the same //! integration surface the Spin adapter's `run_app` calls into. #![cfg(all(feature = "spin", target_arch = "wasm32"))] diff --git a/crates/mocktioneer-cli/Cargo.toml b/crates/mocktioneer-cli/Cargo.toml new file mode 100644 index 0000000..f1d655f --- /dev/null +++ b/crates/mocktioneer-cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "mocktioneer-cli" +version = "0.1.0" +edition = "2021" +publish = false +license.workspace = true + +[dependencies] +mocktioneer-core = { workspace = true } +edgezero-cli = { workspace = true } +clap = { workspace = true } +log = { workspace = true } + +[lints] +workspace = true diff --git a/crates/mocktioneer-cli/src/main.rs b/crates/mocktioneer-cli/src/main.rs new file mode 100644 index 0000000..c6f459f --- /dev/null +++ b/crates/mocktioneer-cli/src/main.rs @@ -0,0 +1,85 @@ +//! Mocktioneer CLI — built on the `edgezero-cli` library. +//! +//! Reuses every built-in edgezero command and adds the **typed** `config` +//! arms parameterised over `MocktioneerConfig`, so `validator` rules run on +//! `config validate` / `config push` / `config diff`. + +use clap::{Parser, Subcommand}; +use edgezero_cli::args::{ + AuthArgs, BuildArgs, ConfigDiffArgs, ConfigPushArgs, ConfigValidateArgs, DeployArgs, + ProvisionArgs, ServeArgs, +}; +use edgezero_cli::DiffExit; +use mocktioneer_core::config::MocktioneerConfig; + +#[derive(Parser, Debug)] +#[command(name = "mocktioneer-cli", about = "mocktioneer edge CLI")] +struct Args { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand, Debug)] +enum Cmd { + /// Sign in / out / status against the adapter's native CLI. + Auth(AuthArgs), + /// Build the project for a target edge. + Build(BuildArgs), + /// Inspect or mutate the typed `mocktioneer.toml` app config. + #[command(subcommand)] + Config(MocktioneerConfigCmd), + /// Deploy to a target edge. + Deploy(DeployArgs), + /// Create the platform resources backing the declared store ids. + Provision(ProvisionArgs), + /// Run a local simulation (adapter-specific). + Serve(ServeArgs), +} + +/// Dispatches `validate`/`push`/`diff` to the typed entry points over +/// `MocktioneerConfig`. +#[derive(Subcommand, Debug)] +enum MocktioneerConfigCmd { + /// Diff `mocktioneer.toml` against the live (or local-emulator) config + /// store. Exits 0 (no changes), 1 (changes with `--exit-code`), 2 (error). + Diff(ConfigDiffArgs), + /// Push `mocktioneer.toml` as a blob envelope to the adapter's config store. + Push(ConfigPushArgs), + /// Validate `edgezero.toml` + `mocktioneer.toml` against `MocktioneerConfig`. + Validate(ConfigValidateArgs), +} + +fn main() { + use std::process; + + edgezero_cli::init_cli_logger(); + let result: Result<(), String> = match Args::parse().cmd { + Cmd::Auth(args) => edgezero_cli::run_auth(&args), + Cmd::Build(args) => edgezero_cli::run_build(&args), + Cmd::Config(MocktioneerConfigCmd::Diff(args)) => { + // `run_config_diff_typed` returns `Result`: a + // non-zero exit code (1 = diff with `--exit-code`; 2 = unsupported) + // exits the process directly, mirroring the generated template. + match edgezero_cli::run_config_diff_typed::(&args) { + Ok(DiffExit { code: 0 }) => Ok(()), + Ok(DiffExit { code }) => process::exit(code), + Err(err) => Err(err), + } + } + Cmd::Config(MocktioneerConfigCmd::Push(args)) => { + edgezero_cli::run_config_push_typed::(&args) + } + Cmd::Config(MocktioneerConfigCmd::Validate(args)) => { + edgezero_cli::run_config_validate_typed::(&args) + } + Cmd::Deploy(args) => edgezero_cli::run_deploy(&args), + Cmd::Provision(args) => edgezero_cli::run_provision(&args), + Cmd::Serve(args) => edgezero_cli::run_serve(&args), + }; + if let Err(err) = result { + log::error!("[mocktioneer] {err}"); + // Exit 2 for all errors so `config diff` errors satisfy the "errors are + // always ≥ 2" contract; push / validate are not 1-vs-2 sensitive. + process::exit(2); + } +} diff --git a/crates/mocktioneer-core/Cargo.toml b/crates/mocktioneer-core/Cargo.toml index b4a9341..c6405dd 100644 --- a/crates/mocktioneer-core/Cargo.toml +++ b/crates/mocktioneer-core/Cargo.toml @@ -6,6 +6,7 @@ publish = false license.workspace = true [dependencies] +anyhow = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } ed25519-dalek = { workspace = true } diff --git a/crates/mocktioneer-core/src/auction.rs b/crates/mocktioneer-core/src/auction.rs index ff49d0f..26e05b7 100644 --- a/crates/mocktioneer-core/src/auction.rs +++ b/crates/mocktioneer-core/src/auction.rs @@ -83,7 +83,8 @@ pub fn standard_or_default((width, height): (i64, i64)) -> (i64, i64) { /// Build an `OpenRTB` bid response for the given request. /// /// - Enforces standard ad sizes (non-standard sizes default to 300x250) -/// - Uses a fixed CPM price ($0.20) +/// - Prices every bid at the supplied `cpm` (callers pass the resolved config +/// value, defaulting to `FIXED_BID_CPM`) /// - Embeds signature verification status, the original request, and a preview /// of the response as HTML comments in each creative /// - The signature badge is rendered inside the creative via the `sig` query param @@ -93,6 +94,7 @@ pub fn build_openrtb_response( req: &OpenRTBRequest, base_host: &str, signature_status: SignatureStatus, + cpm: f64, ) -> OpenRTBResponse { // Build bids without adm let mut bids: Vec = Vec::new(); @@ -111,13 +113,13 @@ pub fn build_openrtb_response( { log::warn!( "imp[{}].ext.mocktioneer.bid is deprecated and ignored; \ - all bids use fixed price ${}", + all bids use the configured price ${}", imp.id, - FIXED_BID_CPM + cpm ); } - let price = FIXED_BID_CPM; + let price = cpm; bids.push(OpenrtbBid { id: bid_id, @@ -220,14 +222,14 @@ pub fn decode_aps_price(encoded: &str) -> Option { /// Build APS TAM response from an APS bid request matching real Amazon API format. /// /// This function generates mock bids for all slots with standard sizes: -/// - Fixed bid price of $0.20 CPM +/// - Prices every slot at the supplied `cpm` (defaulting to `FIXED_BID_CPM`) /// - 100% fill rate for standard sizes /// - Returns contextual format matching real Amazon APS API /// - No creative HTML (APS doesn't return adm field) /// - Generates base64-encoded price strings (recoverable in mock, unlike real APS) #[inline] #[must_use] -pub fn build_aps_response(req: &ApsBidRequest, base_host: &str) -> ApsBidResponse { +pub fn build_aps_response(req: &ApsBidRequest, base_host: &str, cpm: f64) -> ApsBidResponse { let mut slots: Vec = Vec::new(); for slot in &req.slots { @@ -255,8 +257,8 @@ pub fn build_aps_response(req: &ApsBidRequest, base_host: &str) -> ApsBidRespons continue; }; - // Generate bid components using fixed CPM pricing - let price = FIXED_BID_CPM; + // Generate bid components using the resolved CPM + let price = cpm; let impression_id = new_id(); let crid = format!("{}-{}", new_id(), "mocktioneer"); let size_str = format!("{width}x{height}"); @@ -341,7 +343,7 @@ mod tests { }], ..Default::default() }; - let resp = build_openrtb_response(&req, "host.test", test_signature()); + let resp = build_openrtb_response(&req, "host.test", test_signature(), FIXED_BID_CPM); let bid_id = &resp.seatbid[0].bid[0].id; assert_eq!(bid_id.len(), 32); assert!( @@ -365,7 +367,7 @@ mod tests { user_agent: None, timeout: None, }; - let resp = build_aps_response(&req, "mock.test"); + let resp = build_aps_response(&req, "mock.test", FIXED_BID_CPM); let slot = &resp.contextual.slots[0]; // Use decode_aps_price to verify the encoded price @@ -386,7 +388,7 @@ mod tests { user_agent: None, timeout: None, }; - let resp = build_aps_response(&req, "mock.test"); + let resp = build_aps_response(&req, "mock.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); let slot = &resp.contextual.slots[0]; @@ -406,7 +408,7 @@ mod tests { user_agent: None, timeout: None, }; - let resp = build_aps_response(&req, "mock.test"); + let resp = build_aps_response(&req, "mock.test", FIXED_BID_CPM); assert_eq!(resp.contextual.status, Some("ok".to_owned())); assert_eq!(resp.contextual.slots.len(), 1); @@ -433,7 +435,7 @@ mod tests { user_agent: None, timeout: None, }; - let resp = build_aps_response(&req, "mock.test"); + let resp = build_aps_response(&req, "mock.test", FIXED_BID_CPM); // Non-standard sizes should be skipped assert!(resp.contextual.slots.is_empty()); @@ -452,7 +454,7 @@ mod tests { user_agent: None, timeout: None, }; - let resp = build_aps_response(&req, "mock.test"); + let resp = build_aps_response(&req, "mock.test", FIXED_BID_CPM); let slot = &resp.contextual.slots[0]; // Verify targeting keys list @@ -485,7 +487,7 @@ mod tests { }], ..Default::default() }; - let resp = build_openrtb_response(&req, "host.test", test_signature()); + let resp = build_openrtb_response(&req, "host.test", test_signature(), FIXED_BID_CPM); let bid = &resp.seatbid[0].bid[0]; // Non-standard should default to 300x250 assert_eq!(bid.width, Some(300)); @@ -507,7 +509,7 @@ mod tests { }], ..Default::default() }; - let resp = build_openrtb_response(&req, "host.test", test_signature()); + let resp = build_openrtb_response(&req, "host.test", test_signature(), FIXED_BID_CPM); assert_eq!(resp.id, "r1"); assert_eq!(resp.cur.as_deref(), Some("USD")); assert_eq!(resp.seatbid.len(), 1); @@ -560,7 +562,7 @@ mod tests { }], ..Default::default() }; - let resp = build_openrtb_response(&req, "host.test", test_signature()); + let resp = build_openrtb_response(&req, "host.test", test_signature(), FIXED_BID_CPM); let bid = &resp.seatbid[0].bid[0]; assert_eq!(bid.price.to_bits(), FIXED_BID_CPM.to_bits()); assert!(bid.ext.is_none()); @@ -623,4 +625,42 @@ mod tests { assert_eq!(standard_or_default((333, 222)), (300, 250)); assert_eq!(standard_or_default((320, 50)), (320, 50)); } + + #[test] + fn openrtb_uses_supplied_cpm() { + let req = OpenRTBRequest { + id: "rc".to_owned(), + imp: vec![OpenrtbImp { + id: "1".to_owned(), + banner: Some(Banner { + width: Some(300), + height: Some(250), + ..Default::default() + }), + ..Default::default() + }], + ..Default::default() + }; + let resp = build_openrtb_response(&req, "host.test", test_signature(), 0.35); + assert!((resp.seatbid[0].bid[0].price - 0.35).abs() < f64::EPSILON); + } + + #[test] + fn aps_uses_supplied_cpm() { + let req = ApsBidRequest { + pub_id: "test".to_owned(), + slots: vec![ApsSlot { + slot_id: "slot1".to_owned(), + sizes: vec![[300, 250]], + slot_name: None, + }], + page_url: None, + user_agent: None, + timeout: None, + }; + let resp = build_aps_response(&req, "mock.test", 0.35); + let slot = &resp.contextual.slots[0]; + let price = decode_aps_price(slot.amznbid.as_ref().unwrap()).unwrap(); + assert!((price - 0.35).abs() < f64::EPSILON); + } } diff --git a/crates/mocktioneer-core/src/config.rs b/crates/mocktioneer-core/src/config.rs new file mode 100644 index 0000000..c5873b1 --- /dev/null +++ b/crates/mocktioneer-core/src/config.rs @@ -0,0 +1,39 @@ +//! Typed application config, loaded from `mocktioneer.toml`. The TOML file +//! maps 1:1 onto this struct (no `[config]` wrapper). v1 carries a single +//! `bid_cpm` field; `config validate --strict` enforces the rules below. + +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Debug, Deserialize, Serialize, Validate, edgezero_core::AppConfig)] +#[serde(deny_unknown_fields)] +pub struct MocktioneerConfig { + /// Fixed bid CPM in USD. Must be strictly positive. `exclusive_min` also + /// rejects `0.0`, negatives, and `NaN` (any comparison with `NaN` is + /// false); non-finite floats are additionally rejected by edgezero's typed + /// config loader before this runs. + #[validate(range(exclusive_min = 0.0_f64))] + pub bid_cpm: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_positive_finite_cpm() { + let cfg = MocktioneerConfig { bid_cpm: 0.20 }; + cfg.validate().unwrap(); + } + + #[test] + fn rejects_zero_negative_and_nan() { + // `range(exclusive_min = 0.0)` rejects these (NaN fails every + // comparison). `+Inf` passes the range check but is rejected upstream + // by edgezero's non-finite-float loader, so it is not asserted here. + for bad in [0.0_f64, -1.0_f64, f64::NAN] { + let cfg = MocktioneerConfig { bid_cpm: bad }; + assert!(cfg.validate().is_err(), "expected {bad} to be rejected"); + } + } +} diff --git a/crates/mocktioneer-core/src/lib.rs b/crates/mocktioneer-core/src/lib.rs index a07a836..140de5f 100644 --- a/crates/mocktioneer-core/src/lib.rs +++ b/crates/mocktioneer-core/src/lib.rs @@ -1,5 +1,6 @@ pub mod aps; pub mod auction; +pub mod config; pub mod mediation; pub mod openrtb; pub mod render; diff --git a/crates/mocktioneer-core/src/routes.rs b/crates/mocktioneer-core/src/routes.rs index 3f1a27e..1f4c556 100644 --- a/crates/mocktioneer-core/src/routes.rs +++ b/crates/mocktioneer-core/src/routes.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use edgezero_core::action; use edgezero_core::context::RequestContext; use edgezero_core::extractor::{ - ForwardedHost, FromRequest, Headers, ValidatedJson, ValidatedQuery, + AppConfig, ForwardedHost, FromRequest, Headers, ValidatedJson, ValidatedQuery, }; use edgezero_core::http::{ header, response_builder, HeaderMap, HeaderValue, Method, Response, StatusCode, @@ -23,6 +23,7 @@ use crate::aps::ApsBidRequest; use crate::auction::{ build_aps_response, build_openrtb_response, is_standard_size, standard_sizes, }; +use crate::config::MocktioneerConfig; use crate::mediation::{mediate_auction, MediationRequest}; use crate::openrtb::OpenRTBRequest; use crate::render::{ @@ -313,6 +314,7 @@ pub async fn handle_root(ForwardedHost(host): ForwardedHost) -> Result, RequestContext(ctx): RequestContext, ForwardedHost(host): ForwardedHost, ValidatedJson(req): ValidatedJson, @@ -342,7 +344,7 @@ pub async fn handle_openrtb_auction( log::info!("auction id={}, imps={}", req.id, req.imp.len()); // Build response with embedded metadata (signature status + request + response preview) - let resp = build_openrtb_response(&req, &host, signature_status); + let resp = build_openrtb_response(&req, &host, signature_status, cfg.bid_cpm); let body = Body::json(&resp).map_err(|err| { log::error!("Failed to serialize OpenRTB response: {err}"); EdgeError::internal(err) @@ -468,6 +470,7 @@ pub async fn handle_pixel( #[action] pub async fn handle_aps_bid( + AppConfig(cfg): AppConfig, ForwardedHost(host): ForwardedHost, ValidatedJson(req): ValidatedJson, ) -> Result { @@ -477,7 +480,7 @@ pub async fn handle_aps_bid( req.slots.len() ); - let resp = build_aps_response(&req, &host); + let resp = build_aps_response(&req, &host, cfg.bid_cpm); let body = Body::json(&resp).map_err(|err| { log::error!("Failed to serialize APS response: {err}"); EdgeError::internal(err) @@ -995,14 +998,31 @@ fn sanitize_for_log(input: &str, max_len: usize) -> String { #[cfg(test)] mod tests { use super::*; + use crate::auction::decode_aps_price; + use edgezero_core::blob_envelope::BlobEnvelope; use edgezero_core::body::Body; + use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; use edgezero_core::context::RequestContext; use edgezero_core::error::EdgeError; use edgezero_core::http::{request_builder, Method, Response, StatusCode}; use edgezero_core::params::PathParams; use edgezero_core::response::IntoResponse as _; + use edgezero_core::store_registry::{ConfigRegistry, ConfigStoreBinding, StoreRegistry}; use futures::executor::block_on; use std::collections::HashMap; + use std::sync::Arc; + + /// In-memory `ConfigStore` for tests (mirrors app-demo's `MapConfigStore`). + struct MapConfigStore(HashMap); + + // `ConfigStore` is declared `#[async_trait(?Send)]` in edgezero-core, so + // the impl MUST use the same `(?Send)` mode or method signatures won't match. + #[async_trait(?Send)] + impl ConfigStore for MapConfigStore { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + Ok(self.0.get(key).cloned()) + } + } fn response_from(result: Result) -> Response { match result { @@ -1019,10 +1039,86 @@ mod tests { .to_vec() } + /// Assert the fail-loud compatibility contract for a bid route served with + /// no config store bound: `503 Service Unavailable`, a `Retry-After` header, + /// and a JSON body whose `error.kind` is `config_out_of_date`. + fn assert_config_out_of_date(response: Response) { + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + response + .headers() + .get("retry-after") + .and_then(|value| value.to_str().ok()), + Some("60"), + ); + let payload: serde_json::Value = + serde_json::from_slice(&body_bytes(response)).expect("json error body"); + assert_eq!(payload["error"]["kind"], "config_out_of_date"); + } + + /// A valid blob-envelope JSON string wrapping `{ "bid_cpm": }`, + /// matching what `config push` writes for the typed config. + fn config_blob(bid_cpm: f64) -> String { + let data = serde_json::json!({ "bid_cpm": bid_cpm }); + serde_json::to_string(&BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_owned())) + .expect("serialize envelope") + } + + fn config_registry(bid_cpm: f64) -> ConfigRegistry { + let map: HashMap = + [("mocktioneer_config".to_owned(), config_blob(bid_cpm))] + .into_iter() + .collect(); + StoreRegistry::single_id( + "mocktioneer_config".to_owned(), + ConfigStoreBinding { + handle: ConfigStoreHandle::new(Arc::new(MapConfigStore(map))), + default_key: "mocktioneer_config".to_owned(), + }, + ) + } + fn ctx(method: Method, uri: &str, body: Body, params: &[(&str, &str)]) -> RequestContext { - let mut builder = request_builder(); - builder = builder.method(method).uri(uri); - let request = builder.body(body).expect("request"); + ctx_with_cpm(method, uri, body, params, 0.20_f64) + } + + /// A `RequestContext` whose default config store is bound but empty — i.e. a + /// deploy that declared `[stores.config]` but never ran `config push`. The + /// `AppConfig` extractor then fails loud with `503 config_out_of_date`, the + /// documented "must push before serving bids" compatibility contract. + fn ctx_without_pushed_config(method: Method, uri: &str, body: Body) -> RequestContext { + let mut request = request_builder() + .method(method) + .uri(uri) + .body(body) + .expect("request"); + let empty = StoreRegistry::single_id( + "mocktioneer_config".to_owned(), + ConfigStoreBinding { + handle: ConfigStoreHandle::new(Arc::new(MapConfigStore(HashMap::new()))), + default_key: "mocktioneer_config".to_owned(), + }, + ); + request.extensions_mut().insert(empty); + RequestContext::new(request, PathParams::new(HashMap::new())) + } + + /// Like [`ctx`] but seeds the default config store with a `bid_cpm` blob. + /// The auction/APS handlers use the fail-loud `AppConfig` extractor, so a + /// bound, valid blob must be present for them to run. + fn ctx_with_cpm( + method: Method, + uri: &str, + body: Body, + params: &[(&str, &str)], + bid_cpm: f64, + ) -> RequestContext { + let mut request = request_builder() + .method(method) + .uri(uri) + .body(body) + .expect("request"); + request.extensions_mut().insert(config_registry(bid_cpm)); let map = params .iter() .map(|(key, value)| ((*key).to_owned(), (*value).to_owned())) @@ -1030,6 +1126,88 @@ mod tests { RequestContext::new(request, PathParams::new(map)) } + // ---- bid_cpm typed-config wiring ---- + + #[test] + fn auction_uses_seeded_cpm() { + let body = serde_json::json!({ + "id": "rc", + "imp": [{ "id": "1", "banner": { "w": 300_i32, "h": 250_i32 } }] + }); + let ctx = ctx_with_cpm( + Method::POST, + "/openrtb2/auction", + Body::json(&body).expect("json body"), + &[], + 0.35_f64, + ); + let response = response_from(block_on(handle_openrtb_auction(ctx))); + assert_eq!(response.status(), StatusCode::OK); + let payload: serde_json::Value = + serde_json::from_slice(&body_bytes(response)).expect("json"); + let price = payload["seatbid"][0]["bid"][0]["price"] + .as_f64() + .expect("price"); + assert!((price - 0.35).abs() < f64::EPSILON); + } + + #[test] + fn auction_without_config_errors() { + // Fail-loud: with no config store bound the `AppConfig` extractor errors + // (out-of-the-box requires `config push` before serving auctions). + let body = serde_json::json!({ + "id": "rc", + "imp": [{ "id": "1", "banner": { "w": 300_i32, "h": 250_i32 } }] + }); + let ctx = ctx_without_pushed_config( + Method::POST, + "/openrtb2/auction", + Body::json(&body).expect("json body"), + ); + let response = response_from(block_on(handle_openrtb_auction(ctx))); + assert_config_out_of_date(response); + } + + #[test] + fn aps_bid_uses_seeded_cpm() { + let body = serde_json::json!({ + "pubId": "5555", + "slots": [{ "slotID": "slot1", "sizes": [[300_i32, 250_i32]] }] + }); + let ctx = ctx_with_cpm( + Method::POST, + "/e/dtb/bid", + Body::json(&body).expect("json body"), + &[], + 0.35_f64, + ); + let response = response_from(block_on(handle_aps_bid(ctx))); + assert_eq!(response.status(), StatusCode::OK); + let payload: serde_json::Value = + serde_json::from_slice(&body_bytes(response)).expect("json"); + let amznbid = payload["contextual"]["slots"][0]["amznbid"] + .as_str() + .expect("amznbid"); + let price = decode_aps_price(amznbid).expect("decode price"); + assert!((price - 0.35).abs() < f64::EPSILON); + } + + #[test] + fn aps_bid_without_config_errors() { + // Fail-loud parity with the OpenRTB path: store bound but not pushed. + let body = serde_json::json!({ + "pubId": "5555", + "slots": [{ "slotID": "slot1", "sizes": [[300_i32, 250_i32]] }] + }); + let ctx = ctx_without_pushed_config( + Method::POST, + "/e/dtb/bid", + Body::json(&body).expect("json body"), + ); + let response = response_from(block_on(handle_aps_bid(ctx))); + assert_config_out_of_date(response); + } + #[test] fn parse_size_param_parses_suffix() { assert_eq!(parse_size_param("300x250.svg", ".svg"), Some((300, 250))); @@ -1476,7 +1654,7 @@ mod tests { let first = &sizes[0]; assert!(first["width"].is_i64()); assert!(first["height"].is_i64()); - // CPM is no longer included — bid price is fixed at FIXED_BID_CPM + // CPM is not echoed in the response — bid price comes from config (default FIXED_BID_CPM) assert!(first.get("cpm").is_none()); } diff --git a/crates/mocktioneer-core/tests/aps_endpoints.rs b/crates/mocktioneer-core/tests/aps_endpoints.rs index 0aeff48..2e2ad95 100644 --- a/crates/mocktioneer-core/tests/aps_endpoints.rs +++ b/crates/mocktioneer-core/tests/aps_endpoints.rs @@ -1,9 +1,9 @@ use mocktioneer_core::aps::{ApsBidRequest, ApsSlot}; -use mocktioneer_core::auction::build_aps_response; +use mocktioneer_core::auction::{build_aps_response, FIXED_BID_CPM}; #[cfg(test)] mod tests { - use super::build_aps_response; + use super::{build_aps_response, FIXED_BID_CPM}; use super::{ApsBidRequest, ApsSlot}; #[test] @@ -20,7 +20,7 @@ mod tests { timeout: Some(800), }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.status, Some("ok".to_owned())); assert_eq!(resp.contextual.slots.len(), 1); @@ -73,7 +73,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 3); assert_eq!(resp.contextual.slots[0].slot_id, "header"); @@ -103,7 +103,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 0); } @@ -129,7 +129,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); assert_eq!(resp.contextual.slots[0].slot_id, "standard"); @@ -149,7 +149,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "test.host"); + let resp = build_aps_response(&req, "test.host", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); let slot = &resp.contextual.slots[0]; @@ -185,7 +185,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); assert_eq!(resp.contextual.slots[0].size, "970x250"); @@ -205,7 +205,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); assert_eq!(resp.contextual.slots[0].size, "300x250"); @@ -225,7 +225,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); assert_eq!(resp.contextual.slots[0].size, "970x250"); @@ -245,7 +245,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 0); } @@ -260,7 +260,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 0); assert_eq!(resp.contextual.status, Some("ok".to_owned())); @@ -280,7 +280,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); let contextual = &resp.contextual; assert_eq!(contextual.status, Some("ok".to_owned())); @@ -313,7 +313,7 @@ mod tests { timeout: None, }; - let resp = build_aps_response(&req, "mocktioneer.test"); + let resp = build_aps_response(&req, "mocktioneer.test", FIXED_BID_CPM); assert_eq!(resp.contextual.slots.len(), 1); } diff --git a/crates/mocktioneer-core/tests/endpoints.rs b/crates/mocktioneer-core/tests/endpoints.rs index 2759e62..5297525 100644 --- a/crates/mocktioneer-core/tests/endpoints.rs +++ b/crates/mocktioneer-core/tests/endpoints.rs @@ -1,16 +1,55 @@ #[cfg(test)] mod tests { + use async_trait::async_trait; use edgezero_core::app::App; + use edgezero_core::blob_envelope::BlobEnvelope; use edgezero_core::body::Body; + use edgezero_core::config_store::{ConfigStore, ConfigStoreError, ConfigStoreHandle}; use edgezero_core::http::{ header, request_builder, HeaderValue, Method, Request, Response, StatusCode, }; + use edgezero_core::store_registry::{ConfigRegistry, ConfigStoreBinding, StoreRegistry}; use futures::executor::block_on; + use std::collections::HashMap; + use std::sync::Arc; + + /// In-memory config store returning a blob envelope by key. + struct MapConfigStore(HashMap); + + #[async_trait(?Send)] + impl ConfigStore for MapConfigStore { + async fn get(&self, key: &str) -> Result, ConfigStoreError> { + Ok(self.0.get(key).cloned()) + } + } fn app() -> App { mocktioneer_core::build_app() } + /// Build a `ConfigRegistry` whose default `mocktioneer_config` store holds a + /// blob envelope for `{ "bid_cpm": }` — what `config push` writes. + /// The auction/APS handlers use the fail-loud `AppConfig` extractor, so the + /// router needs this bound to serve those routes. + fn config_registry(bid_cpm: f64) -> ConfigRegistry { + let data = serde_json::json!({ "bid_cpm": bid_cpm }); + let blob = + serde_json::to_string(&BlobEnvelope::new(data, "2026-01-01T00:00:00Z".to_owned())) + .expect("serialize envelope"); + let store = MapConfigStore( + [("mocktioneer_config".to_owned(), blob)] + .into_iter() + .collect(), + ); + StoreRegistry::single_id( + "mocktioneer_config".to_owned(), + ConfigStoreBinding { + handle: ConfigStoreHandle::new(Arc::new(store)), + default_key: "mocktioneer_config".to_owned(), + }, + ) + } + fn make_request(method: Method, uri: &str, body: Body) -> Request { request_builder() .method(method) @@ -103,6 +142,7 @@ mod tests { request .headers_mut() .insert(header::HOST, HeaderValue::from_static("test.local")); + request.extensions_mut().insert(config_registry(0.20_f64)); let response = dispatch(&app, request); assert_eq!(response.status(), StatusCode::OK); let content_type = response diff --git a/docs/.gitignore b/docs/.gitignore index 57a09c3..097c229 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,4 @@ node_modules .vitepress/dist .vitepress/cache +.vitepress/.temp diff --git a/docs/.prettierignore b/docs/.prettierignore index 94aa6e0..c43c06b 100644 --- a/docs/.prettierignore +++ b/docs/.prettierignore @@ -1,3 +1,5 @@ .vitepress/cache .vitepress/dist +.vitepress/.temp node_modules +superpowers/ diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index b6e18c3..fc4a16c 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -5,6 +5,9 @@ export default defineConfig({ title: 'Mocktioneer', description: 'Deterministic OpenRTB banner bidder for edge platforms', base: '/mocktioneer/', + // Internal design specs/plans live under docs/superpowers/ but are not + // published documentation — keep them out of the built site. + srcExclude: ['**/superpowers/**'], themeConfig: { // https://vitepress.dev/reference/default-theme-config nav: [ diff --git a/docs/api/aps-bid.md b/docs/api/aps-bid.md index a621c61..79b8954 100644 --- a/docs/api/aps-bid.md +++ b/docs/api/aps-bid.md @@ -2,6 +2,18 @@ The `/e/dtb/bid` endpoint accepts Amazon Publisher Services (APS) Transparent Ad Marketplace bid requests and returns bids in APS format. +::: warning Requires a pushed config +Like the OpenRTB endpoint, this reads `bid_cpm` from the typed app config via the +fail-loud `AppConfig` extractor and returns an error until the config blob is +pushed once for the target adapter: + +```bash +cargo run -p mocktioneer-cli -- config push --adapter axum --yes +``` + +See [Configuration › Typed App Config](/guide/configuration#typed-app-config). +::: + ## Endpoint ``` diff --git a/docs/api/index.md b/docs/api/index.md index 2608ea0..9df9cd2 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -66,7 +66,7 @@ All responses include CORS headers (`Access-Control-Allow-Origin: *`, etc.). See ## Supported Ad Sizes {#supported-sizes} -Mocktioneer supports these standard IAB sizes. All auction bids use a fixed price of `$0.20` CPM. +Mocktioneer supports these standard IAB sizes. All auction bids use a default price of `$0.20` CPM (configurable via `bid_cpm` in mocktioneer.toml). | Size | Name | | ------- | ----------------------------- | diff --git a/docs/api/openrtb-auction.md b/docs/api/openrtb-auction.md index 7401b21..c7bf8d0 100644 --- a/docs/api/openrtb-auction.md +++ b/docs/api/openrtb-auction.md @@ -2,6 +2,20 @@ The `/openrtb2/auction` endpoint accepts OpenRTB 2.x bid requests and returns deterministic bid responses. +::: warning Requires a pushed config +This endpoint reads `bid_cpm` from the typed app config via the fail-loud +`AppConfig` extractor, so it returns an error (`config_out_of_date`) until the +config blob has been pushed once for the target adapter: + +```bash +cargo run -p mocktioneer-cli -- config push --adapter axum --yes +``` + +See [Configuration › Typed App Config](/guide/configuration#typed-app-config) +for per-adapter details. (Static, `/pixel`, and `/_/sizes` endpoints work +without it.) +::: + ## Endpoint ``` @@ -139,7 +153,7 @@ Size is determined in this order: ## Pricing -Mocktioneer returns a fixed bid price of `$0.20` CPM for auction responses. +Mocktioneer returns a default bid price of `$0.20` CPM for auction responses (configurable via `bid_cpm` in mocktioneer.toml). If `imp[].ext.mocktioneer.bid` is present, it is ignored. diff --git a/docs/eslint.config.js b/docs/eslint.config.js index 50481a7..d991fe3 100644 --- a/docs/eslint.config.js +++ b/docs/eslint.config.js @@ -3,7 +3,12 @@ import tseslint from 'typescript-eslint' export default [ { - ignores: ['.vitepress/cache/**', '.vitepress/dist/**', 'node_modules/**'], + ignores: [ + '.vitepress/cache/**', + '.vitepress/dist/**', + '.vitepress/.temp/**', + 'node_modules/**', + ], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/docs/guide/adapters/axum.md b/docs/guide/adapters/axum.md index b232234..3fefbd0 100644 --- a/docs/guide/adapters/axum.md +++ b/docs/guide/adapters/axum.md @@ -14,15 +14,24 @@ The Axum adapter runs Mocktioneer as a native Rust HTTP server. It's the recomme ## Quick Start ```bash +# One-time: create the local config and push it (auction/APS are fail-loud) +cp mocktioneer.toml.example mocktioneer.toml +cargo run -p mocktioneer-cli -- config push --adapter axum --yes + cargo run -p mocktioneer-adapter-axum ``` -The server starts at `http://127.0.0.1:8787`. +The server starts at `http://127.0.0.1:8787`. Axum reads the pushed blob from +`./.edgezero/local-config-mocktioneer_config.json` **once at startup**, so +re-run `config push` and restart after changing `bid_cpm`. (Static/pixel/sizes +endpoints work without a pushed config.) -## Using EdgeZero CLI +## Using the CLI ```bash edgezero-cli serve --adapter axum +# or, in-repo (no external install): +cargo run -p mocktioneer-cli -- serve --adapter axum ``` This executes the command defined in `edgezero.toml`: @@ -65,7 +74,11 @@ Logs are written to stdout. Adjust `level` for more or less verbosity: ## Docker Deployment -This repository does not include a Dockerfile. If you add one, you can containerize the Axum adapter like this: +The repository ships a multi-stage `Dockerfile` (also built and pushed to +`ghcr.io/stackpop/mocktioneer` by CI). The build **seeds the default typed-config +blob** into `/app/.edgezero/`, so the OpenRTB/APS endpoints work out of the box +(they read `bid_cpm` via the fail-loud `AppConfig` extractor and would otherwise +503 until a blob is pushed): ```bash # Build the image @@ -78,6 +91,14 @@ docker run -p 8787:8787 mocktioneer:latest docker run -p 3000:8787 mocktioneer:latest ``` +To override the baked-in config, mount your own blob over the seeded file: + +```bash +docker run -p 8787:8787 \ + -v "$PWD/.edgezero/local-config-mocktioneer_config.json:/app/.edgezero/local-config-mocktioneer_config.json:ro" \ + mocktioneer:latest +``` + ## Development Workflow ### Hot Reloading diff --git a/docs/guide/adapters/cloudflare.md b/docs/guide/adapters/cloudflare.md index 4c7df87..2ece695 100644 --- a/docs/guide/adapters/cloudflare.md +++ b/docs/guide/adapters/cloudflare.md @@ -44,8 +44,10 @@ The Cloudflare adapter runs Mocktioneer on Cloudflare Workers, providing global Run locally using Wrangler's local mode: ```bash -# Using EdgeZero CLI +# Using the CLI edgezero-cli serve --adapter cloudflare +# or, in-repo (no external install): +cargo run -p mocktioneer-cli -- serve --adapter cloudflare # Or directly wrangler dev --config crates/mocktioneer-adapter-cloudflare/wrangler.toml @@ -53,6 +55,33 @@ wrangler dev --config crates/mocktioneer-adapter-cloudflare/wrangler.toml This starts a local server that emulates the Workers environment. +::: warning Config is KV-backed on Cloudflare +Unlike Axum (local file) and Fastly, Cloudflare reads `bid_cpm` from a **KV +namespace**, so `/openrtb2/auction` and `/e/dtb/bid` are fail-loud until the +config blob is pushed into that namespace. The KV namespace must be bound in +`wrangler.toml` as `[[kv_namespaces]] binding = "mocktioneer_config"`: + +- **Remote/deploy:** `mocktioneer-cli provision --adapter cloudflare` creates the + namespace and writes the binding into `wrangler.toml`, then + `config push --adapter cloudflare`. +- **Local (`wrangler dev`):** `provision` is remote-only (it calls the Cloudflare + API). For a local-only KV, add the binding to `wrangler.toml` manually with a + placeholder id, then push to local state: + + ```toml + [[kv_namespaces]] + binding = "mocktioneer_config" + id = "local-dev-placeholder" + ``` + + ```bash + cp mocktioneer.toml.example mocktioneer.toml + cargo run -p mocktioneer-cli -- config push --adapter cloudflare --local + ``` + +Static/pixel/sizes endpoints work without any of this. +::: + ## Building ```bash diff --git a/docs/guide/adapters/fastly.md b/docs/guide/adapters/fastly.md index 35830e0..686eb9c 100644 --- a/docs/guide/adapters/fastly.md +++ b/docs/guide/adapters/fastly.md @@ -30,11 +30,24 @@ The Fastly adapter runs Mocktioneer on Fastly's Compute platform, providing glob ## Local Development +::: tip Push the config first +`/openrtb2/auction` and `/e/dtb/bid` are fail-loud — push the typed config to +the Fastly config store before serving (static/pixel/sizes work without it): + +```bash +cp mocktioneer.toml.example mocktioneer.toml +cargo run -p mocktioneer-cli -- config push --adapter fastly --local +``` + +::: + Run locally using Fastly's Viceroy runtime: ```bash -# Using EdgeZero CLI +# Using the CLI edgezero-cli serve --adapter fastly +# or, in-repo (no external install): +cargo run -p mocktioneer-cli -- serve --adapter fastly # Or directly fastly compute serve -C crates/mocktioneer-adapter-fastly @@ -62,16 +75,45 @@ target/wasm32-wasip1/release/mocktioneer-adapter-fastly.wasm ### First-Time Setup +`/openrtb2/auction` and `/e/dtb/bid` read `bid_cpm` from the **remote Fastly +config store** via the fail-loud `AppConfig` extractor. Deploying alone is not +enough — without the config store created, seeded and linked, those endpoints +return `503 config_out_of_date`. Run the three steps in this order: + ```bash -edgezero-cli deploy --adapter fastly +# 1. Create the remote config store and append [setup.config_stores.*] to fastly.toml +cargo run -p mocktioneer-cli -- provision --adapter fastly + +# 2. Push the typed config blob into the store just created +cp mocktioneer.toml.example mocktioneer.toml # if you haven't already +cargo run -p mocktioneer-cli -- config push --adapter fastly --yes + +# 3. Deploy — creating the service consumes [setup] and links the store to it +cargo run -p mocktioneer-cli -- deploy --adapter fastly ``` -The CLI will prompt you to: +The deploy will prompt you to: 1. Create a new service or select existing 2. Configure the domain 3. Deploy the WASM bundle +::: warning Already-deployed services skip `[setup]` +Fastly consumes `[setup.config_stores.*]` **only when `deploy` creates a new +service**. If `fastly.toml` already declares a `service_id`, the store is created +in your account but is **not linked** to the service, so the runtime cannot open +it. `provision` detects this and prints the exact one-shot command to finish the +job — look up the store id with `fastly config-store list --json`, then: + +```bash +fastly resource-link create --service-id= --resource-id= \ + --version=latest --autoclone --name=mocktioneer_config +``` + +The link clones the active version, so live traffic is unaffected until you run +`fastly service-version activate`. +::: + ### Subsequent Deployments ```bash @@ -82,6 +124,16 @@ edgezero-cli deploy --adapter fastly fastly compute deploy -C crates/mocktioneer-adapter-fastly ``` +### Updating the Config + +`bid_cpm` lives in the config store, not the WASM bundle — after editing +`mocktioneer.toml`, **re-push**; no redeploy is needed: + +```bash +cargo run -p mocktioneer-cli -- config diff --adapter fastly # preview +cargo run -p mocktioneer-cli -- config push --adapter fastly --yes +``` + ## Configuration ### Build Settings diff --git a/docs/guide/adapters/index.md b/docs/guide/adapters/index.md index 1b83e60..b206360 100644 --- a/docs/guide/adapters/index.md +++ b/docs/guide/adapters/index.md @@ -87,6 +87,26 @@ edgezero-cli build --adapter fastly The CLI reads `edgezero.toml` and executes the appropriate commands for each adapter. If you don't have `edgezero-cli`, use the direct adapter commands on the pages below. +### In-repo `mocktioneer-cli` + +This repository also vendors a thin `mocktioneer-cli` built on the EdgeZero CLI +library — no separate install needed: + +```bash +# serve / build / deploy (same as edgezero-cli) +cargo run -p mocktioneer-cli -- serve --adapter cloudflare + +# typed config (lives only here — validated against MocktioneerConfig) +cp mocktioneer.toml.example mocktioneer.toml # gitignored per-env copy +cargo run -p mocktioneer-cli -- config validate --strict +cargo run -p mocktioneer-cli -- config diff --adapter axum +cargo run -p mocktioneer-cli -- config push --adapter axum --yes +``` + +`serve`/`build`/`deploy`/`auth`/`provision` work from either the external +`edgezero-cli` or `mocktioneer-cli`; the typed `config validate` / `config diff` / +`config push` commands are only in `mocktioneer-cli`. + ## Common Configuration All adapters read from the same `edgezero.toml`: diff --git a/docs/guide/architecture.md b/docs/guide/architecture.md index c2e9043..795f43d 100644 --- a/docs/guide/architecture.md +++ b/docs/guide/architecture.md @@ -168,6 +168,6 @@ Template rendering: ## Supported Sizes -Mocktioneer supports 13 standard IAB ad sizes, each with a fixed CPM price. See the [full size list with pricing](/api/#supported-sizes) in the API reference. +Mocktioneer supports 13 standard IAB ad sizes, each with a default CPM price (configurable via `bid_cpm` in mocktioneer.toml). See the [full size list with pricing](/api/#supported-sizes) in the API reference. Non-standard sizes return 404 for static assets or are coerced to 300x250 for auction responses. Use the [`/_/sizes`](/api/#sizes-endpoint) endpoint to get the current list programmatically. diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index 353aa76..ff6e214 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -14,6 +14,9 @@ middleware = [ "edgezero_core::middleware::RequestLogger", "mocktioneer_core::routes::Cors" ] + +[stores.config] +ids = ["mocktioneer_config"] ``` ## App Section @@ -26,6 +29,23 @@ The `[app]` section defines the core application: | `entry` | Path to the core crate | | `middleware` | List of middleware to apply to all routes | +## Config Store + +The `[stores.config]` section declares the logical config store(s) backing the +typed app config (`mocktioneer.toml`): + +```toml +[stores.config] +ids = ["mocktioneer_config"] +``` + +The `mocktioneer_config` store holds the typed config blob (see the +`MocktioneerConfig` struct). Seed it per adapter with +`mocktioneer-cli config push --adapter `; the OpenRTB/APS handlers read it +at runtime via the **fail-loud `AppConfig` extractor** — a `config push` is +required before those endpoints serve (see [Typed App Config](#typed-app-config) +below). + ## HTTP Triggers Routes are defined as `[[triggers.http]]` blocks: @@ -36,7 +56,7 @@ id = "openrtb_auction" path = "/openrtb2/auction" methods = ["POST"] handler = "mocktioneer_core::routes::handle_openrtb_auction" -adapters = ["axum", "cloudflare", "fastly"] +adapters = ["axum", "cloudflare", "fastly", "spin"] ``` | Field | Description | @@ -64,9 +84,33 @@ adapters = ["axum", "cloudflare", "fastly"] | `/sync/start` | GET | `handle_sync_start` | EC pixel sync initiation | | `/sync/done` | GET | `handle_sync_done` | EC pixel sync callback | | `/resolve` | GET | `handle_resolve` | EC pull sync resolution | +| `/_mocktioneer/manifest` | GET | `introspection::manifest` | Full manifest as JSON | +| `/_mocktioneer/config` | GET | `introspection::config` | Effective app config | +| `/_mocktioneer/routes` | GET | `introspection::routes` | Route table as JSON | All routes also have OPTIONS handlers for CORS preflight. +### Introspection Routes + +The `/_mocktioneer/{manifest,config,routes}` endpoints are **framework-supplied** +handlers from `edgezero_core::introspection`, bound like any other route in +`edgezero.toml`: + +- **`manifest`** — the full `edgezero.toml` manifest as JSON (baked at compile + time; `[environment.secrets]` values are redacted). +- **`config`** — the effective app config from the default config store (the + pushed `bid_cpm` blob's `.data`), with any `#[secret]` fields left as + unresolved key-name references (secret-safe). +- **`routes`** — the live route table as `[{ "method", "path" }]`. + +::: warning Unauthenticated +These endpoints are unauthenticated wherever bound — restrict access at the +network/middleware layer before exposing them publicly. `manifest` emits +`[environment.variables]` values verbatim (only `[environment.secrets]` are +redacted), so keep secrets out of `[environment.variables]`. Mocktioneer +declares no `[environment]` section, so nothing sensitive is exposed today. +::: + ## Adapter Configuration Each adapter has its own configuration section: @@ -105,7 +149,7 @@ profile = "release" features = ["fastly"] [adapters.fastly.commands] -build = "cargo build --release --target wasm32-wasip1 -p mocktioneer-adapter-fastly" +build = "fastly compute build -C crates/mocktioneer-adapter-fastly" serve = "fastly compute serve -C crates/mocktioneer-adapter-fastly" deploy = "fastly compute deploy -C crates/mocktioneer-adapter-fastly" @@ -128,15 +172,97 @@ profile = "release" features = ["cloudflare"] [adapters.cloudflare.commands] -build = "cargo build --release --target wasm32-unknown-unknown -p mocktioneer-adapter-cloudflare" -serve = "wrangler dev --config crates/mocktioneer-adapter-cloudflare/wrangler.toml" -deploy = "wrangler publish --config crates/mocktioneer-adapter-cloudflare/wrangler.toml" +build = "wrangler build --cwd crates/mocktioneer-adapter-cloudflare" +serve = "wrangler dev --cwd crates/mocktioneer-adapter-cloudflare" +deploy = "wrangler deploy --cwd crates/mocktioneer-adapter-cloudflare" [adapters.cloudflare.logging] level = "info" echo_stdout = true ``` +### Spin Adapter + +Spin targets `wasm32-wasip2` (spin-sdk 6). Its config store is KV-backed, so the +`serve` command passes a `--runtime-config-file` declaring the KV label +(`spin deploy` is plugin-mediated and provisions KV itself, so it takes no +runtime-config flag): + +::: warning `spin up` currently blocked (upstream) +The Spin adapter **compiles** to a `wasm32-wasip2` component and passes the +router-level contract tests (run under `wasmtime`), but `spin up` on today's +Spin runtimes (e.g. 3.6.3) fails to link: `spin-sdk 6.0.0` pulls +`wasi:http@0.3.0-rc`, which no released Spin provides (they expose +`wasi:http@0.2`). This is a spin-sdk/runtime ABI mismatch to be resolved +upstream in EdgeZero's `spin-sdk` pin — the Fastly / Cloudflare / Axum adapters +are unaffected. +::: + +```toml +[adapters.spin.adapter] +crate = "crates/mocktioneer-adapter-spin" +manifest = "crates/mocktioneer-adapter-spin/spin.toml" + +[adapters.spin.build] +target = "wasm32-wasip2" +profile = "release" +features = ["spin"] + +[adapters.spin.commands] +build = "spin build --from crates/mocktioneer-adapter-spin/spin.toml" +serve = "spin up --from crates/mocktioneer-adapter-spin/spin.toml --runtime-config-file crates/mocktioneer-adapter-spin/runtime-config.toml" +deploy = "spin deploy --from crates/mocktioneer-adapter-spin/spin.toml" + +[adapters.spin.logging] +level = "info" +echo_stdout = true +``` + +## Typed App Config + +`mocktioneer.toml` (repo root) maps 1:1 onto the `MocktioneerConfig` struct — +there is no `[config]` wrapper: + +```toml +bid_cpm = 0.20 +``` + +`mocktioneer.toml` is **gitignored** (per-environment); the repo commits +`mocktioneer.toml.example` as the template. Create your local copy first: + +```bash +cp mocktioneer.toml.example mocktioneer.toml # then edit bid_cpm as needed +``` + +Validate it, preview the diff against the live store, then push it: + +```bash +cargo run -p mocktioneer-cli -- config validate --strict +cargo run -p mocktioneer-cli -- config diff --adapter axum +cargo run -p mocktioneer-cli -- config push --adapter axum --yes +``` + +`config push` writes the whole struct as a single **blob envelope** (canonical +JSON + a SHA for drift detection) under the store's key — for axum that's +`.edgezero/local-config-mocktioneer_config.json` as +`{ "mocktioneer_config": "" }`. The handlers read it back through the +typed `AppConfig` extractor. + +::: warning Config is required at runtime +The OpenRTB (`/openrtb2/auction`) and APS (`/e/dtb/bid`) endpoints read +`bid_cpm` via the fail-loud `AppConfig` extractor. **A fresh deploy must run +`config push` once** before those endpoints serve bids — until then they return +an error (the static/creative/pixel endpoints are unaffected). `bid_cpm = 0.20` +is the shipped default value, not a runtime fallback. +::: + +Any key can be overridden via the `MOCKTIONEER__` env overlay +(e.g. `MOCKTIONEER__BID_CPM=0.35`). The overlay is applied **when the CLI loads +`mocktioneer.toml`** (during `config validate` / `diff` / `push`), so it changes +the value pushed into the config-store blob — set it before `config push`. It +does **not** mutate config the running server has already loaded; re-push to +roll out a change. + ## Logging Configuration | Field | Description | diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 19fbf71..10515a7 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -38,8 +38,31 @@ Or run it from a local EdgeZero checkout: cargo run --manifest-path /path/to/edgezero/Cargo.toml -p edgezero-cli --features cli -- --help ``` +Alternatively, this repo vendors `mocktioneer-cli` (built on the EdgeZero CLI +library) — no external install needed, and it adds the typed config commands: + +```bash +cargo run -p mocktioneer-cli -- --help +# `mocktioneer.toml` is gitignored (created in Running Locally below); validate +# the committed template so this works on a fresh checkout. +cargo run -p mocktioneer-cli -- config validate --strict --app-config mocktioneer.toml.example +``` + ## Running Locally +> **Seed the app config first.** The OpenRTB (`/openrtb2/auction`) and APS +> (`/e/dtb/bid`) endpoints read `bid_cpm` from the typed config via the +> fail-loud `AppConfig` extractor, so they error until you push the config once +> per adapter. `mocktioneer.toml` is gitignored (per-env); create it from the +> committed template, then push: +> +> ```bash +> cp mocktioneer.toml.example mocktioneer.toml # then edit bid_cpm if desired +> cargo run -p mocktioneer-cli -- config push --adapter axum --yes +> ``` +> +> The root, `/static/*`, `/pixel`, and `/_/sizes` endpoints work without it. + ### Option 1: Native Axum Server (Recommended for Development) The fastest way to get started: @@ -50,10 +73,12 @@ cargo run -p mocktioneer-adapter-axum The server starts at `http://127.0.0.1:8787`. -### Option 2: Using EdgeZero CLI +### Option 2: Using the CLI ```bash edgezero-cli serve --adapter axum +# or, in-repo (no external install): +cargo run -p mocktioneer-cli -- serve --adapter axum ``` ### Option 3: Fastly Local Development diff --git a/docs/guide/what-is-mocktioneer.md b/docs/guide/what-is-mocktioneer.md index a02c7f6..0d272d2 100644 --- a/docs/guide/what-is-mocktioneer.md +++ b/docs/guide/what-is-mocktioneer.md @@ -44,14 +44,14 @@ Mocktioneer provides: ## Key Features -| Feature | Description | -| --------------- | --------------------------------------------- | -| Multi-platform | Runs on Fastly, Cloudflare, and native Axum | -| Manifest-driven | Single `edgezero.toml` configures everything | -| Fixed pricing | Always returns `$0.20` CPM for generated bids | -| Standard sizes | Supports common IAB ad sizes | -| Cookie tracking | Optional pixel tracking with `mtkid` cookie | -| CORS enabled | Works with browser-based clients | +| Feature | Description | +| --------------- | ------------------------------------------------------------------------------------------------- | +| Multi-platform | Runs on Fastly, Cloudflare, and native Axum | +| Manifest-driven | Single `edgezero.toml` configures everything | +| Fixed pricing | Returns a default `$0.20` CPM for generated bids (configurable via `bid_cpm` in mocktioneer.toml) | +| Standard sizes | Supports common IAB ad sizes | +| Cookie tracking | Optional pixel tracking with `mtkid` cookie | +| CORS enabled | Works with browser-based clients | ## How It Works diff --git a/docs/integrations/index.md b/docs/integrations/index.md index fceb906..389dd8c 100644 --- a/docs/integrations/index.md +++ b/docs/integrations/index.md @@ -50,7 +50,7 @@ Mocktioneer acts as a drop-in replacement for real bidders during development an - Same request always produces same response - No flaky tests due to bidder variability -- Fixed $0.20 bids for predictable floor testing +- Default $0.20 bids (configurable via `bid_cpm` in mocktioneer.toml) for predictable floor testing ### No External Dependencies @@ -150,7 +150,7 @@ Test price handling and floor logic: } ``` -Mocktioneer always bids at a fixed `$0.20` CPM and does not evaluate `bidfloor` itself. Use this predictable price to test that your SSP or mediation layer correctly filters bids below the floor threshold. +Mocktioneer bids at a default `$0.20` (configurable via `bid_cpm` in mocktioneer.toml) CPM and does not evaluate `bidfloor` itself. Use this predictable price to test that your SSP or mediation layer correctly filters bids below the floor threshold. ### Identity Pipeline Testing diff --git a/docs/integrations/prebid-server.md b/docs/integrations/prebid-server.md index 73c3ccf..453cf4b 100644 --- a/docs/integrations/prebid-server.md +++ b/docs/integrations/prebid-server.md @@ -8,6 +8,14 @@ The Mocktioneer adapter is not yet merged into upstream Prebid Server. Use the S **[github.com/stackpop/prebid-server](https://github.com/stackpop/prebid-server)** ::: +::: tip Push the config first +Mocktioneer's auction endpoint (`/openrtb2/auction`) only returns bids after the +typed config has been pushed once (`mocktioneer-cli config push --adapter `). +Do this before adding Mocktioneer as a bidder, or Prebid Server will see errors +instead of bids. See +[Configuration › Typed App Config](/guide/configuration#typed-app-config). +::: + ## Configuration ### Host Configuration @@ -91,7 +99,7 @@ Override the endpoint for specific requests: | ---------- | ------------------ | ------ | ------------------------- | | `endpoint` | `imp[].ext.bidder` | string | Override auction endpoint | -Mocktioneer always returns a fixed bid price of `$0.20` CPM. +Mocktioneer returns a default bid price of `$0.20` CPM (configurable via `bid_cpm` in mocktioneer.toml). ## Response Handling @@ -273,7 +281,7 @@ Include Mocktioneer alongside real bidders: ### Price Floor Testing -Test that your SSP enforces floors correctly (Mocktioneer always bids at a fixed `$0.20` and does not evaluate `bidfloor` itself): +Test that your SSP enforces floors correctly (Mocktioneer bids at a default `$0.20` (configurable via `bid_cpm` in mocktioneer.toml) and does not evaluate `bidfloor` itself): ```json { diff --git a/docs/integrations/prebidjs.md b/docs/integrations/prebidjs.md index 69d30cd..406630d 100644 --- a/docs/integrations/prebidjs.md +++ b/docs/integrations/prebidjs.md @@ -8,6 +8,14 @@ The Mocktioneer adapter is not yet merged into upstream Prebid.js. Use the Stack **[github.com/stackpop/Prebid.js](https://github.com/stackpop/Prebid.js)** ::: +::: tip Push the config first +Mocktioneer's auction endpoint (`/openrtb2/auction`) only returns bids after the +typed config has been pushed once (`mocktioneer-cli config push --adapter `). +Do this before pointing Prebid.js at your Mocktioneer instance, or the adapter +will see errors instead of bids. See +[Configuration › Typed App Config](/guide/configuration#typed-app-config). +::: + ## Installation Clone and build from the Stackpop fork: @@ -74,7 +82,7 @@ params: { | ---------- | ------ | -------- | --------------------------- | | `endpoint` | string | No | Custom auction endpoint URL | -Mocktioneer always returns a fixed bid price of `$0.20` CPM. +Mocktioneer returns a default bid price of `$0.20` CPM (configurable via `bid_cpm` in mocktioneer.toml). ## Example Page diff --git a/docs/package-lock.json b/docs/package-lock.json index e483ef1..946ad47 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9,25 +9,25 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/node": "^24.10", - "eslint": "^9.17.0", + "@eslint/js": "^10.0.1", + "@types/node": "^26.1.0", + "eslint": "^10.6.0", "prettier": "^3.4.2", - "typescript-eslint": "^8.57.0", + "typescript-eslint": "^8.62.1", "vitepress": "^1.5.0" } }, "node_modules/@algolia/abtesting": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.13.0.tgz", - "integrity": "sha512-Zrqam12iorp3FjiKMXSTpedGYznZ3hTEOAr2oCxI8tbF8bS1kQHClyDYNq/eV0ewMNLyFkgZVWjaS+8spsOYiQ==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.1.tgz", + "integrity": "sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" @@ -83,41 +83,41 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.47.0.tgz", - "integrity": "sha512-aOpsdlgS9xTEvz47+nXmw8m0NtUiQbvGWNuSEb7fA46iPL5FxOmOUZkh8PREBJpZ0/H8fclSc7BMJCVr+Dn72w==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz", + "integrity": "sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-analytics": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.47.0.tgz", - "integrity": "sha512-EcF4w7IvIk1sowrO7Pdy4Ako7x/S8+nuCgdk6En+u5jsaNQM4rTT09zjBPA+WQphXkA2mLrsMwge96rf6i7Mow==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.1.tgz", + "integrity": "sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-common": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.47.0.tgz", - "integrity": "sha512-Wzg5Me2FqgRDj0lFuPWFK05UOWccSMsIBL2YqmTmaOzxVlLZ+oUqvKbsUSOE5ud8Fo1JU7JyiLmEXBtgDKzTwg==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.1.tgz", + "integrity": "sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw==", "dev": true, "license": "MIT", "engines": { @@ -125,160 +125,161 @@ } }, "node_modules/@algolia/client-insights": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.47.0.tgz", - "integrity": "sha512-Ci+cn/FDIsDxSKMRBEiyKrqybblbk8xugo6ujDN1GSTv9RIZxwxqZYuHfdLnLEwLlX7GB8pqVyqrUSlRnR+sJA==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.1.tgz", + "integrity": "sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-personalization": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.47.0.tgz", - "integrity": "sha512-gsLnHPZmWcX0T3IigkDL2imCNtsQ7dR5xfnwiFsb+uTHCuYQt+IwSNjsd8tok6HLGLzZrliSaXtB5mfGBtYZvQ==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.1.tgz", + "integrity": "sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.47.0.tgz", - "integrity": "sha512-PDOw0s8WSlR2fWFjPQldEpmm/gAoUgLigvC3k/jCSi/DzigdGX6RdC0Gh1RR1P8Cbk5KOWYDuL3TNzdYwkfDyA==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz", + "integrity": "sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.47.0.tgz", - "integrity": "sha512-b5hlU69CuhnS2Rqgsz7uSW0t4VqrLMLTPbUpEl0QVz56rsSwr1Sugyogrjb493sWDA+XU1FU5m9eB8uH7MoI0g==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", + "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/ingestion": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.47.0.tgz", - "integrity": "sha512-WvwwXp5+LqIGISK3zHRApLT1xkuEk320/EGeD7uYy+K8WwDd5OjXnhjuXRhYr1685KnkvWkq1rQ/ihCJjOfHpQ==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.1.tgz", + "integrity": "sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/monitoring": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.47.0.tgz", - "integrity": "sha512-j2EUFKAlzM0TE4GRfkDE3IDfkVeJdcbBANWzK16Tb3RHz87WuDfQ9oeEW6XiRE1/bEkq2xf4MvZesvSeQrZRDA==", + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.1.tgz", + "integrity": "sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/recommend": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.47.0.tgz", - "integrity": "sha512-+kTSE4aQ1ARj2feXyN+DMq0CIDHJwZw1kpxIunedkmpWUg8k3TzFwWsMCzJVkF2nu1UcFbl7xsIURz3Q3XwOXA==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.1.tgz", + "integrity": "sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.47.0.tgz", - "integrity": "sha512-Ja+zPoeSA2SDowPwCNRbm5Q2mzDvVV8oqxCQ4m6SNmbKmPlCfe30zPfrt9ho3kBHnsg37pGucwOedRIOIklCHw==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz", + "integrity": "sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0" + "@algolia/client-common": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-fetch": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.47.0.tgz", - "integrity": "sha512-N6nOvLbaR4Ge+oVm7T4W/ea1PqcSbsHR4O58FJ31XtZjFPtOyxmnhgCmGCzP9hsJI6+x0yxJjkW5BMK/XI8OvA==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz", + "integrity": "sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0" + "@algolia/client-common": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.47.0.tgz", - "integrity": "sha512-z1oyLq5/UVkohVXNDEY70mJbT/sv/t6HYtCvCwNrOri6pxBJDomP9R83KOlwcat+xqBQEdJHjbrPh36f1avmZA==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz", + "integrity": "sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg==", "dev": true, "license": "MIT", "dependencies": { - "@algolia/client-common": "5.47.0" + "@algolia/client-common": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -286,9 +287,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -296,13 +297,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -312,14 +313,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -810,131 +811,129 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.2.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -964,9 +963,9 @@ } }, "node_modules/@iconify-json/simple-icons": { - "version": "1.2.68", - "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.68.tgz", - "integrity": "sha512-bQPl1zuZlX6AnofreA1v7J+hoPncrFMppqGboe/SH54jZO37meiBUGBqNOxEpc0HKfZGxJaVVJwZd4gdMYu3hw==", + "version": "1.2.88", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.88.tgz", + "integrity": "sha512-+cvi1qCuvReL29ehi6t62L4fb7GDXe+UlGHFcsJcV7I2l9wtqn9XE2IBKcDr3CI5iGUGS5ISnXv699pSGpyx1Q==", "dev": true, "license": "CC0-1.0", "dependencies": { @@ -988,9 +987,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", "cpu": [ "arm" ], @@ -1002,9 +1001,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", "cpu": [ "arm64" ], @@ -1016,9 +1015,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", "cpu": [ "arm64" ], @@ -1030,9 +1029,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", "cpu": [ "x64" ], @@ -1044,9 +1043,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", "cpu": [ "arm64" ], @@ -1058,9 +1057,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", "cpu": [ "x64" ], @@ -1072,9 +1071,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", "cpu": [ "arm" ], @@ -1086,9 +1085,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", "cpu": [ "arm" ], @@ -1100,9 +1099,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", "cpu": [ "arm64" ], @@ -1114,9 +1113,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", "cpu": [ "arm64" ], @@ -1128,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", "cpu": [ "loong64" ], @@ -1142,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ "loong64" ], @@ -1156,9 +1155,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ "ppc64" ], @@ -1170,9 +1169,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ "ppc64" ], @@ -1184,9 +1183,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ "riscv64" ], @@ -1198,9 +1197,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", "cpu": [ "riscv64" ], @@ -1212,9 +1211,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -1226,9 +1225,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -1240,9 +1239,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -1254,9 +1253,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", "cpu": [ "x64" ], @@ -1268,9 +1267,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -1282,9 +1281,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ "arm64" ], @@ -1296,9 +1295,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ "ia32" ], @@ -1310,9 +1309,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -1324,9 +1323,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", "cpu": [ "x64" ], @@ -1424,10 +1423,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1484,13 +1490,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~8.3.0" } }, "node_modules/@types/unist": { @@ -1508,20 +1515,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.62.1.tgz", + "integrity": "sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/type-utils": "8.62.1", + "@typescript-eslint/utils": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1531,9 +1538,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.62.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1547,16 +1554,17 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.62.1.tgz", + "integrity": "sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3" }, "engines": { @@ -1568,18 +1576,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.62.1.tgz", + "integrity": "sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.62.1", + "@typescript-eslint/types": "^8.62.1", "debug": "^4.4.3" }, "engines": { @@ -1590,18 +1598,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.62.1.tgz", + "integrity": "sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1612,9 +1620,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.62.1.tgz", + "integrity": "sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==", "dev": true, "license": "MIT", "engines": { @@ -1625,21 +1633,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.62.1.tgz", + "integrity": "sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1650,13 +1658,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.62.1.tgz", + "integrity": "sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -1668,21 +1676,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.62.1.tgz", + "integrity": "sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.62.1", + "@typescript-eslint/tsconfig-utils": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/visitor-keys": "8.62.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1692,59 +1700,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.62.1.tgz", + "integrity": "sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.62.1", + "@typescript-eslint/types": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1755,17 +1724,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.62.1.tgz", + "integrity": "sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.62.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1776,23 +1745,10 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", "dev": true, "license": "ISC" }, @@ -1811,77 +1767,77 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", - "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.27", - "entities": "^7.0.0", + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", - "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", - "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.27", - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27", + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.6", + "postcss": "^8.5.15", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", - "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/devtools-api": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", - "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-kit": "^7.7.9" + "@vue/devtools-kit": "^7.7.10" } }, "node_modules/@vue/devtools-kit": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", - "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/devtools-shared": "^7.7.9", + "@vue/devtools-shared": "^7.7.10", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", @@ -1891,9 +1847,9 @@ } }, "node_modules/@vue/devtools-shared": { - "version": "7.7.9", - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", - "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1901,57 +1857,57 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", - "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", "dev": true, "license": "MIT", "dependencies": { - "@vue/shared": "3.5.27" + "@vue/shared": "3.5.39" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", - "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", - "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", "dev": true, "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.27", - "@vue/runtime-core": "3.5.27", - "@vue/shared": "3.5.27", + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", - "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" }, "peerDependencies": { - "vue": "3.5.27" + "vue": "3.5.39" } }, "node_modules/@vue/shared": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", - "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", "dev": true, "license": "MIT" }, @@ -2062,11 +2018,12 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2085,9 +2042,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2102,61 +2059,42 @@ } }, "node_modules/algoliasearch": { - "version": "5.47.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.47.0.tgz", - "integrity": "sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==", + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", + "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@algolia/abtesting": "1.13.0", - "@algolia/client-abtesting": "5.47.0", - "@algolia/client-analytics": "5.47.0", - "@algolia/client-common": "5.47.0", - "@algolia/client-insights": "5.47.0", - "@algolia/client-personalization": "5.47.0", - "@algolia/client-query-suggestions": "5.47.0", - "@algolia/client-search": "5.47.0", - "@algolia/ingestion": "1.47.0", - "@algolia/monitoring": "1.47.0", - "@algolia/recommend": "5.47.0", - "@algolia/requester-browser-xhr": "5.47.0", - "@algolia/requester-fetch": "5.47.0", - "@algolia/requester-node-http": "5.47.0" + "@algolia/abtesting": "1.21.1", + "@algolia/client-abtesting": "5.55.1", + "@algolia/client-analytics": "5.55.1", + "@algolia/client-common": "5.55.1", + "@algolia/client-insights": "5.55.1", + "@algolia/client-personalization": "5.55.1", + "@algolia/client-query-suggestions": "5.55.1", + "@algolia/client-search": "5.55.1", + "@algolia/ingestion": "1.55.1", + "@algolia/monitoring": "1.55.1", + "@algolia/recommend": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" }, "engines": { "node": ">= 14.0.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "18 || 20 || >=22" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -2168,24 +2106,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">=6" + "node": "18 || 20 || >=22" } }, "node_modules/ccount": { @@ -2199,23 +2129,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/character-entities-html4": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", @@ -2238,26 +2151,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2269,13 +2162,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -2436,33 +2322,34 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", + "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", "dev": true, "license": "MIT", + "peer": true, + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -2472,8 +2359,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2481,7 +2367,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -2496,48 +2382,50 @@ } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2692,6 +2580,7 @@ "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "tabbable": "^6.4.0" } @@ -2724,29 +2613,6 @@ "node": ">=10.13.0" } }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hast-util-to-html": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", @@ -2813,23 +2679,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2883,19 +2732,6 @@ "dev": true, "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2957,13 +2793,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3098,16 +2927,19 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minisearch": { @@ -3132,9 +2964,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -3219,19 +3051,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3267,11 +3086,12 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3280,9 +3100,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", "dev": true, "funding": [ { @@ -3300,7 +3120,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3309,9 +3129,9 @@ } }, "node_modules/preact": { - "version": "10.28.2", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", - "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", + "version": "10.29.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", + "integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==", "dev": true, "license": "MIT", "funding": { @@ -3330,9 +3150,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.9.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.4.tgz", + "integrity": "sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==", "dev": true, "license": "MIT", "bin": { @@ -3346,9 +3166,9 @@ } }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "dev": true, "license": "MIT", "funding": { @@ -3393,16 +3213,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -3411,13 +3221,13 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -3427,31 +3237,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" } }, @@ -3464,9 +3274,9 @@ "peer": true }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -3562,19 +3372,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -3588,35 +3385,22 @@ "node": ">=16" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", "dev": true, "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -3637,9 +3421,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -3663,9 +3447,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "peer": true, @@ -3678,16 +3462,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "version": "8.62.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.62.1.tgz", + "integrity": "sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" + "@typescript-eslint/eslint-plugin": "8.62.1", + "@typescript-eslint/parser": "8.62.1", + "@typescript-eslint/typescript-estree": "8.62.1", + "@typescript-eslint/utils": "8.62.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3698,13 +3482,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "dev": true, "license": "MIT" }, @@ -3827,6 +3611,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3924,17 +3709,18 @@ } }, "node_modules/vue": { - "version": "3.5.27", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", - "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.27", - "@vue/compiler-sfc": "3.5.27", - "@vue/runtime-dom": "3.5.27", - "@vue/server-renderer": "3.5.27", - "@vue/shared": "3.5.27" + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" }, "peerDependencies": { "typescript": "*" diff --git a/docs/package.json b/docs/package.json index 75f2842..c26234d 100644 --- a/docs/package.json +++ b/docs/package.json @@ -14,11 +14,11 @@ "lint:fix": "eslint . --fix" }, "devDependencies": { - "@types/node": "^24.10", - "eslint": "^9.17.0", - "@eslint/js": "^9.17.0", - "typescript-eslint": "^8.57.0", + "@eslint/js": "^10.0.1", + "@types/node": "^26.1.0", + "eslint": "^10.6.0", "prettier": "^3.4.2", + "typescript-eslint": "^8.62.1", "vitepress": "^1.5.0" } } diff --git a/docs/superpowers/plans/2026-06-15-edgezero-extensible-cli-adaptation.md b/docs/superpowers/plans/2026-06-15-edgezero-extensible-cli-adaptation.md new file mode 100644 index 0000000..220487b --- /dev/null +++ b/docs/superpowers/plans/2026-06-15-edgezero-extensible-cli-adaptation.md @@ -0,0 +1,962 @@ +# Edgezero #269 (Extensible CLI) Adaptation — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +> **⚠️ Superseded in part by spec R8 (blob app-config sync to edgezero `89f59266`).** +> This plan was executed, then edgezero advanced past the pin with the **blob +> app-config cutover**. The runtime-read design below (Task 6: +> `cpm_from_lookup` / `resolve_bid_cpm` / per-leaf `get("bid_cpm")` / graceful +> `None → FIXED_BID_CPM` fallback) is **obsolete**. As implemented, the handlers +> use the **fail-loud `AppConfig` extractor** (config is one +> SHA-gated blob envelope under the store key; a `config push` is required before +> auction/APS serve). The CLI also gained `config diff` (Task 8), and the CI +> assertion reads into the envelope (Task 12). See spec §3.5 (R8) and the source +> for the authoritative behaviour. + +**Goal:** Adapt Mocktioneer to the breaking edgezero #269 API (extensible CLI, dropped `run_app` manifest arg, Spin SDK 6 / wasip2) and adopt typed `AppConfig`. (R8: the typed config is read at runtime via the fail-loud `AppConfig` extractor — `bid_cpm` requires a `config push`; `FIXED_BID_CPM` is the builder default, not a runtime fallback.) + +**Architecture:** Pin the six `edgezero-*` git deps to `feature/extensible-cli`; fix every adapter entrypoint; migrate the Spin adapter to `spin-sdk ~6.0` / `wasm32-wasip2`; add a `MocktioneerConfig` typed-config struct + `mocktioneer.toml` + a `mocktioneer-cli` crate that mirrors edgezero's generated `-cli`; thread a resolved `cpm` through the OpenRTB and APS bid builders; wire docs/CI/Docker/ignore files. + +**Tech Stack:** Rust (edition 2021, workspace), edgezero framework, `spin-sdk` 6, `clap` 4, `validator`, `anyhow`, VitePress docs, GitHub Actions. + +**Reference spec:** `docs/superpowers/specs/2026-06-11-edgezero-extensible-cli-adaptation-design.md` + +**Branch:** `feature/edgezero-extensible-cli` (already created off `main`). + +**Local iteration tip:** building against the unmerged `feature/extensible-cli` pulls git deps over the network. For fast local iteration you may symlink `.cargo/config.toml.local` (which path-patches `../edgezero`) — but **do not commit** it, and ensure the sibling `../edgezero` checkout is on `feature/extensible-cli`. CI uses the git pin with `--locked`. + +--- + +## File Structure + +| File | Responsibility | Action | +| --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------- | +| `docs/.prettierignore`, `docs/.vitepress/config.*` | Keep internal specs/plans out of the docs format gate + published site | Modify | +| `Cargo.toml` | Workspace dep pins (`feature/extensible-cli`), `spin-sdk ~6.0`, add `clap`; add `mocktioneer-cli` member | Modify | +| `crates/mocktioneer-core/Cargo.toml` | Add `anyhow` dep | Modify | +| `crates/mocktioneer-adapter-{axum,cloudflare,fastly}/src/*` | Drop `include_str!(manifest)` arg | Modify | +| `crates/mocktioneer-adapter-spin/{src/lib.rs,src/main.rs,Cargo.toml,spin.toml,tests/contract.rs,runtime-config.toml}` | Spin SDK 6 / wasip2 migration + KV config backing | Modify/Create | +| `edgezero.toml` | `[stores.config]`, spin `target = wasip2`, spin commands `--runtime-config-file` | Modify | +| `crates/mocktioneer-core/src/config.rs` | `MocktioneerConfig` typed config | Create | +| `crates/mocktioneer-core/src/lib.rs` | `pub mod config;` | Modify | +| `crates/mocktioneer-core/src/auction.rs` | Thread `cpm: f64` into bid builders | Modify | +| `crates/mocktioneer-core/src/routes.rs` | Fail-loud `AppConfig` extractor on both handlers (R8) | Modify | +| `mocktioneer.toml` | Typed config values (default `bid_cpm = 0.20`) | Create | +| `crates/mocktioneer-cli/{Cargo.toml,src/main.rs}` | Custom CLI mirroring edgezero `-cli` | Create | +| `Dockerfile` | Pre-copy `mocktioneer-cli` manifest before `cargo fetch` | Modify | +| `.gitignore` | Ignore `.edgezero/` | Modify | +| `docs/**`, `CLAUDE.md`, `.claude/...`, `README.md`, `tests/playwright/**` | wasip2 + pricing + CLI-story docs | Modify | +| `.github/workflows/test.yml` | Spin wasip2 matrix + config-validate gate | Modify | + +--- + +## Task 0: Keep `docs/superpowers/**` out of the docs format gate & site + +**Why first:** the format CI job runs `prettier --check .` inside `docs/`, which fails on the spec/plan markdown; VitePress also builds them into the published site. Do this before anything else so the spec and this plan don't break CI. + +**Files:** + +- Modify: `docs/.prettierignore` +- Modify: `docs/.vitepress/config.mts` (or `.ts`/`.js` — whichever exists) +- Test: `cd docs && npm run format` + +- [ ] **Step 1: Add `superpowers/` AND the build temp dir to the docs Prettier ignore** + +`docs/.prettierignore` currently lists `.vitepress/cache`, `.vitepress/dist`, `node_modules`. Append both the specs dir and the VitePress build temp dir (a `git`-ignore does NOT stop Prettier/ESLint from scanning a working-tree dir — the tool ignores must be set explicitly): + +``` +superpowers/ +.vitepress/.temp +``` + +- [ ] **Step 2: Exclude internal specs/plans from the VitePress build** + +Find the config file: `ls docs/.vitepress/config.*`. In its `defineConfig({ ... })` object, add a top-level `srcExclude` key (merge if one already exists): + +```ts + srcExclude: ['**/superpowers/**'], +``` + +- [ ] **Step 3: Ignore the VitePress build temp dir in ESLint AND git** + +`npm run build` generates `.vitepress/.temp/` (≈31 JS files) which **ESLint will scan and fail on** (≈700+ errors) unless the flat config ignores it — and the ESLint ignore list is separate from `.prettierignore` and `.gitignore`. In `docs/eslint.config.js`, extend the `ignores` array (currently `['.vitepress/cache/**', '.vitepress/dist/**', 'node_modules/**']`) to add `.vitepress/.temp/**`: + +```js + ignores: [ + '.vitepress/cache/**', + '.vitepress/dist/**', + '.vitepress/.temp/**', + 'node_modules/**', + ], +``` + +Also add `.vitepress/.temp` to `docs/.gitignore` (currently `node_modules`, `.vitepress/dist`, `.vitepress/cache`): + +``` +.vitepress/.temp +``` + +`docs/.vitepress/dist` is already gitignored and untracked — nothing to remove. Confirm: `git check-ignore docs/.vitepress/dist && echo ignored` → `ignored`. + +- [ ] **Step 4: Build FIRST, then verify format + lint pass with generated files present** + +Generate the temp/dist dirs, THEN run the gates (the prior revision only checked format _before_ a build, so it missed the generated-file failures): + +Run: `cd docs && npm ci >/dev/null 2>&1 && npm run build && npm run format && npm run lint` +Expected: all PASS (the `.temp`/`dist`/`superpowers` ignores hold). + +Assert no spec/plan leaked into the build output: + +Run: `find docs/.vitepress/dist docs/.vitepress/.temp -path '*superpowers*' -print -quit` +Expected: **no output**. If anything prints, `srcExclude` is mis-scoped — fix the glob (e.g. `'**/superpowers/**'` relative to the VitePress `srcDir`) and rebuild. + +- [ ] **Step 5: Commit** + +```bash +git add docs/.prettierignore docs/eslint.config.js docs/.vitepress docs/.gitignore .gitignore +git commit -m "docs: exclude superpowers + vitepress build temp from prettier/eslint/vitepress" +``` + +--- + +## Task 1: Pin deps to `feature/extensible-cli` + workspace deps + +**Files:** + +- Modify: `Cargo.toml` (`[workspace.dependencies]`) +- Modify: `crates/mocktioneer-core/Cargo.toml` + +- [ ] **Step 1: Repin the six edgezero deps and bump `spin-sdk`** + +In `Cargo.toml` `[workspace.dependencies]`, change every `branch = "main"` on the `edgezero-*` lines to `branch = "feature/extensible-cli"` (6 lines: adapter-axum, adapter-cloudflare, adapter-fastly, adapter-spin, cli, core). Then replace the `spin-sdk` line: + +```toml +spin-sdk = { version = "~6.0", default-features = false, features = ["http", "key-value", "variables"] } +``` + +- [ ] **Step 2: Add `clap` to workspace deps** + +Add to `[workspace.dependencies]` (alphabetical, near `base64`): + +```toml +clap = { version = "4", features = ["derive"] } +``` + +- [ ] **Step 3: Add `anyhow` to core** + +In `crates/mocktioneer-core/Cargo.toml`, add to `[dependencies]` (alphabetical, before `async-trait`): + +```toml +anyhow = { workspace = true } +``` + +- [ ] **Step 4: Resolve deps & regenerate `Cargo.lock`** + +Run: `cargo check -p mocktioneer-core 2>&1 | tail -30` +Expected: dependency **resolution succeeds** (git deps fetched, `Cargo.lock` rewritten) and `mocktioneer-core` compiles (core doesn't touch `run_app`). If you instead see resolution errors (e.g. branch not found), fix the branch name before continuing. + +- [ ] **Step 5: Commit** + +```bash +git add Cargo.toml crates/mocktioneer-core/Cargo.toml Cargo.lock +git commit -m "build: pin edgezero deps to feature/extensible-cli, bump spin-sdk to 6, add clap/anyhow" +``` + +--- + +## Task 2: Fix axum/cloudflare/fastly entrypoints (drop manifest arg) + +**Files:** + +- Modify: `crates/mocktioneer-adapter-axum/src/main.rs:7` +- Modify: `crates/mocktioneer-adapter-cloudflare/src/lib.rs:11-17` +- Modify: `crates/mocktioneer-adapter-fastly/src/main.rs:16` + +- [ ] **Step 1: Axum — remove the manifest arg** + +In `crates/mocktioneer-adapter-axum/src/main.rs`, change: + +```rust + if let Err(err) = run_app::(include_str!("../../../edgezero.toml")) { +``` + +to: + +```rust + if let Err(err) = run_app::() { +``` + +- [ ] **Step 2: Cloudflare — remove the manifest arg** + +In `crates/mocktioneer-adapter-cloudflare/src/lib.rs`, change the call to: + +```rust + edgezero_adapter_cloudflare::run_app::(req, env, ctx).await +``` + +- [ ] **Step 3: Fastly — remove the manifest arg** + +In `crates/mocktioneer-adapter-fastly/src/main.rs`, change: + +```rust + edgezero_adapter_fastly::run_app::(include_str!("../../../edgezero.toml"), req) +``` + +to: + +```rust + edgezero_adapter_fastly::run_app::(req) +``` + +- [ ] **Step 4: Verify native + wasm targets type-check (Spin still broken — that's Task 3)** + +Run: `cargo check -p mocktioneer-adapter-axum` +Expected: PASS. + +Run: `cargo check -p mocktioneer-adapter-fastly --features fastly --target wasm32-wasip1` +Expected: PASS. + +Run: `cargo check -p mocktioneer-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add crates/mocktioneer-adapter-axum crates/mocktioneer-adapter-cloudflare crates/mocktioneer-adapter-fastly +git commit -m "feat: drop run_app manifest arg for axum/cloudflare/fastly (edgezero #269)" +``` + +--- + +## Task 3: Migrate the Spin adapter to spin-sdk 6 / wasm32-wasip2 + +**Files:** + +- Modify: `crates/mocktioneer-adapter-spin/src/lib.rs` +- Modify: `crates/mocktioneer-adapter-spin/spin.toml` +- Create: `crates/mocktioneer-adapter-spin/runtime-config.toml` +- Modify: `crates/mocktioneer-adapter-spin/src/main.rs` +- Modify: `crates/mocktioneer-adapter-spin/tests/contract.rs` + +- [ ] **Step 1: Rewrite `src/lib.rs` for `#[http_service]` + `Request`** + +Replace the entire file with (mirrors edgezero's `templates/src/lib.rs.hbs`, dropping `no_main` and the manifest arg): + +```rust +#![cfg_attr( + target_arch = "wasm32", + allow( + unsafe_code, + reason = "spin's #[http_service] macro generates the unsafe wasm export" + ) +)] + +#[cfg(target_arch = "wasm32")] +use mocktioneer_core::MocktioneerApp; +#[cfg(target_arch = "wasm32")] +use spin_sdk::http::{IntoResponse, Request}; +#[cfg(target_arch = "wasm32")] +use spin_sdk::http_service; + +#[cfg(target_arch = "wasm32")] +#[http_service] +async fn handle(req: Request) -> anyhow::Result { + edgezero_adapter_spin::run_app::(req).await +} +``` + +- [ ] **Step 2: Point `spin.toml` at wasip2 + declare the config KV store** + +In `crates/mocktioneer-adapter-spin/spin.toml`, replace the `[component.mocktioneer]` and build blocks with: + +```toml +[component.mocktioneer] +source = "../../target/wasm32-wasip2/release/mocktioneer_adapter_spin.wasm" +allowed_outbound_hosts = ["https://*:*"] +key_value_stores = ["mocktioneer_config"] + +[component.mocktioneer.build] +command = "cargo build --target wasm32-wasip2 --release" +watch = ["src/**/*.rs", "Cargo.toml"] +``` + +- [ ] **Step 3: Create `runtime-config.toml`** + +Create `crates/mocktioneer-adapter-spin/runtime-config.toml`: + +```toml +# Spin runtime config: declares the KV labels the component may open. +# The `mocktioneer_config` label backs the EdgeZero `[stores.config]` +# store; the default SQLite backend persists to `.spin/sqlite_key_value.db`. +[key_value_store.mocktioneer_config] +type = "spin" +``` + +- [ ] **Step 4: Update the host-side stub message in `src/main.rs`** + +Replace both `wasm32-wasip1` mentions with `wasm32-wasip2`: + +```rust +#[expect( + clippy::print_stderr, + reason = "host-side stub that exists solely to remind the operator to target wasm32-wasip2" +)] +fn main() { + eprintln!("Run `spin up` or target wasm32-wasip2 to execute mocktioneer-adapter-spin."); +} +``` + +- [ ] **Step 5: Update the contract-test comment to wasip2** + +In `crates/mocktioneer-adapter-spin/tests/contract.rs`, change the doc-comment line `end-to-end under \`wasm32-wasip1\` via the \`wasmtime\` runner`to`wasm32-wasip2`. The `#![cfg(all(feature = "spin", target_arch = "wasm32"))]` gate is unchanged (covers wasip2). + +- [ ] **Step 6: Verify the Spin wasm build + native workspace check** + +Build `--release` to match the `spin.toml` `source` path (`target/wasm32-wasip2/release/...`): + +Run: `rustup target add wasm32-wasip2 >/dev/null 2>&1; cargo build --release -p mocktioneer-adapter-spin --features spin --target wasm32-wasip2` +Expected: PASS — produces `target/wasm32-wasip2/release/mocktioneer_adapter_spin.wasm`. If the SDK-6 macro needs a tweak, fix per the compiler. + +If the `spin` CLI is installed, also prove the manifest's source path resolves: + +Run: `command -v spin >/dev/null && spin build --from crates/mocktioneer-adapter-spin/spin.toml || echo "spin CLI not installed — skipping"` +Expected: PASS or the skip note. + +Run: `cargo check --workspace` +Expected: PASS (native). + +- [ ] **Step 7: Commit** + +```bash +git add crates/mocktioneer-adapter-spin +git commit -m "feat: migrate spin adapter to spin-sdk 6 / wasm32-wasip2 (edgezero #269)" +``` + +--- + +## Task 4: Manifest — declare config store, spin wasip2 target, runtime-config + +**Files:** + +- Modify: `edgezero.toml` + +- [ ] **Step 1: Declare the config store** + +Add a top-level `[stores.config]` section to `edgezero.toml` (place it after `[app]`, before the triggers): + +```toml +[stores.config] +ids = ["mocktioneer_config"] +``` + +- [ ] **Step 2: Spin build target → wasip2** + +In `edgezero.toml`, in `[adapters.spin.build]`, change `target = "wasm32-wasip1"` to: + +```toml +target = "wasm32-wasip2" +``` + +- [ ] **Step 3: Spin serve/deploy commands → pass `--runtime-config-file`** + +In `[adapters.spin.commands]`, change `serve` and `deploy` to reference the runtime config: + +```toml +build = "spin build --from crates/mocktioneer-adapter-spin/spin.toml" +deploy = "spin deploy --from crates/mocktioneer-adapter-spin/spin.toml --runtime-config-file crates/mocktioneer-adapter-spin/runtime-config.toml" +serve = "spin up --from crates/mocktioneer-adapter-spin/spin.toml --runtime-config-file crates/mocktioneer-adapter-spin/runtime-config.toml" +``` + +- [ ] **Step 4: Verify the manifest still compiles (validated at compile time by `app!`)** + +Run: `cargo check -p mocktioneer-core` +Expected: PASS. If `manifest.validate()` rejects a table, fix per its error message. + +- [ ] **Step 5: Commit** + +```bash +git add edgezero.toml +git commit -m "feat: declare [stores.config] + spin wasip2/runtime-config in manifest" +``` + +--- + +## Task 5: `MocktioneerConfig` typed config struct + +**Files:** + +- Create: `crates/mocktioneer-core/src/config.rs` +- Modify: `crates/mocktioneer-core/src/lib.rs` + +- [ ] **Step 1: Write the failing validation tests** + +Create `crates/mocktioneer-core/src/config.rs`: + +```rust +//! Typed application config, loaded from `mocktioneer.toml`. The TOML file +//! maps 1:1 onto this struct (no `[config]` wrapper). v1 carries a single +//! `bid_cpm` field; `config validate --strict` enforces the rules below. + +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Debug, Deserialize, Serialize, Validate, edgezero_core::AppConfig)] +#[serde(deny_unknown_fields)] +pub struct MocktioneerConfig { + /// Fixed bid CPM in USD. Strictly positive (`exclusive_min` rejects 0.0, + /// negatives, and NaN; non-finite floats also rejected by edgezero's + /// loader). `_f64` suffix satisfies the strict `default_numeric_fallback` + /// clippy lint. + #[validate(range(exclusive_min = 0.0_f64))] + pub bid_cpm: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn accepts_positive_finite_cpm() { + let cfg = MocktioneerConfig { bid_cpm: 0.20 }; + assert!(cfg.validate().is_ok()); + } + + #[test] + fn rejects_zero_negative_and_non_finite() { + for bad in [0.0_f64, -1.0, f64::NAN, f64::INFINITY] { + let cfg = MocktioneerConfig { bid_cpm: bad }; + assert!(cfg.validate().is_err(), "expected {bad} to be rejected"); + } + } +} +``` + +- [ ] **Step 2: Wire the module** + +In `crates/mocktioneer-core/src/lib.rs`, add to the top module list (alphabetical, after `aps`): + +```rust +pub mod config; +``` + +- [ ] **Step 3: Run the tests (expect FAIL first if the derive isn't wired, then PASS)** + +Run: `cargo test -p mocktioneer-core config::tests` +Expected: PASS. (If the `edgezero_core::AppConfig` derive path is wrong, the compile error tells you — it is re-exported from `edgezero-core`, no `edgezero-macros` dep needed.) + +- [ ] **Step 4: Commit** + +```bash +git add crates/mocktioneer-core/src/config.rs crates/mocktioneer-core/src/lib.rs +git commit -m "feat: add MocktioneerConfig typed config (bid_cpm, validated)" +``` + +--- + +## Task 6: Thread `cpm` through the bid builders + runtime resolution + +> **⚠️ Read model below is OBSOLETE (spec R8).** Keep the `cpm: f64` builder +> parameter threading, but the runtime read is NOT `cpm_from_lookup` / +> `resolve_bid_cpm` / `get("bid_cpm")`. As implemented: `handle_openrtb_auction` +> and `handle_aps_bid` take `AppConfig(cfg): AppConfig` and +> pass `cfg.bid_cpm` to the builders — fail-loud (errors with no pushed config). +> Tests seed a **blob envelope** via a `ConfigRegistry` fixture +> (`StoreRegistry::single_id` + `ConfigStoreBinding` + `BlobEnvelope::new`). + +**Files:** + +- Modify: `crates/mocktioneer-core/src/auction.rs` (`build_openrtb_response`, `build_aps_response`, tests) +- Modify: `crates/mocktioneer-core/src/routes.rs` (`handle_openrtb_auction`, `handle_aps_bid`, new helpers, imports) + +### As built (R8 fail-loud blob model) + +> The original step-by-step (pure `cpm_from_lookup`, `resolve_bid_cpm`, and a +> `None → FIXED_BID_CPM` fallback) was **removed** — it predates the blob +> cutover and must not be implemented. The behaviour that shipped: + +1. **Builders take `cpm: f64`.** `build_openrtb_response(req, host, sig, cpm)` + and `build_aps_response(req, host, cpm)` replace their direct `FIXED_BID_CPM` + reads with the parameter; `FIXED_BID_CPM` stays the builders' default arg + + the shipped `mocktioneer.toml` value. Every `auction.rs` / + `tests/aps_endpoints.rs` call site passes `FIXED_BID_CPM` explicitly (and do + **not** touch the separate `mediation.rs::build_openrtb_response`). + +2. **Handlers read typed config via the fail-loud `AppConfig` extractor.** Add + `use edgezero_core::extractor::AppConfig;` and + `use crate::config::MocktioneerConfig;`, then: + + ```rust + #[action] + pub async fn handle_openrtb_auction( + RequestContext(ctx): RequestContext, + ForwardedHost(host): ForwardedHost, + ValidatedJson(req): ValidatedJson, + AppConfig(cfg): AppConfig, + ) -> Result { /* … build_openrtb_response(&req, &host, sig, cfg.bid_cpm) */ } + + #[action] + pub async fn handle_aps_bid( + ForwardedHost(host): ForwardedHost, + ValidatedJson(req): ValidatedJson, + AppConfig(cfg): AppConfig, + ) -> Result { /* … build_aps_response(&req, &host, cfg.bid_cpm) */ } + ``` + + With no store bound / no blob pushed, the extractor errors + (`config_out_of_date`) — auction/APS require a `config push`. `FIXED_BID_CPM` + is dropped from `routes.rs` imports (no longer referenced there). + +3. **Tests seed a blob.** The `routes.rs` test `ctx()` helper seeds the default + config store with a blob envelope (`StoreRegistry::single_id` + + `ConfigStoreBinding` + `BlobEnvelope::new` over an in-memory `MapConfigStore` + holding `{ "mocktioneer_config": "" }`), so the + existing handler tests keep exercising their 400/422 paths. Add + `auction_uses_seeded_cpm` (seed `0.35` → assert bid `price == 0.35`) and + `auction_without_config_errors` (no registry → handler errors). The + `tests/endpoints.rs` router auction test inserts the same registry. + +4. **Verify + commit.** + + Run: `cargo test -p mocktioneer-core && cargo clippy -p mocktioneer-core --all-targets --all-features -- -D warnings` + Expected: PASS. + + ```bash + git add crates/mocktioneer-core/src/auction.rs crates/mocktioneer-core/src/routes.rs crates/mocktioneer-core/tests/endpoints.rs + git commit -m "feat: resolve bid_cpm from typed config (fail-loud AppConfig extractor, blob model)" + ``` + +--- + +## Task 7: `mocktioneer.toml` typed config file + +**Files:** + +- Create: `mocktioneer.toml` (repo root, next to `edgezero.toml`) + +- [ ] **Step 1: Create the config file** + +`config validate`/`push` resolve `.toml` next to the manifest from `[app].name = "mocktioneer"`. Create `mocktioneer.toml`: + +```toml +# Typed application config for Mocktioneer (maps 1:1 onto MocktioneerConfig). +# `bid_cpm` is the default; override per-environment via `config push` or the +# `MOCKTIONEER__BID_CPM` env overlay. Default keeps historical $0.20 behavior. +bid_cpm = 0.20 +``` + +- [ ] **Step 2: Commit (validation happens in Task 8 once the CLI exists)** + +```bash +git add mocktioneer.toml +git commit -m "feat: add mocktioneer.toml typed config (bid_cpm default 0.20)" +``` + +--- + +## Task 8: `mocktioneer-cli` crate + +> **R8 addition:** the crate also wires the new `config diff` command — +> `MocktioneerConfigCmd::Diff(ConfigDiffArgs)` dispatching +> `edgezero_cli::run_config_diff_typed::` (returns `DiffExit`; +> non-zero codes `process::exit`, all errors exit `2`). `new` is intentionally +> omitted (see R7). + +**Files:** + +- Create: `crates/mocktioneer-cli/Cargo.toml` +- Create: `crates/mocktioneer-cli/src/main.rs` +- Modify: `Cargo.toml` (`[workspace].members`) + +- [ ] **Step 1: Add the crate to the workspace members** + +In root `Cargo.toml`, add to `[workspace].members` (after the spin adapter line): + +```toml + "crates/mocktioneer-cli", +``` + +- [ ] **Step 2: Create `crates/mocktioneer-cli/Cargo.toml`** + +```toml +[package] +name = "mocktioneer-cli" +version = "0.1.0" +edition = "2021" +publish = false +license.workspace = true + +[dependencies] +mocktioneer-core = { workspace = true } +edgezero-cli = { workspace = true } +clap = { workspace = true } +log = { workspace = true } + +[lints] +workspace = true +``` + +- [ ] **Step 3: Create `crates/mocktioneer-cli/src/main.rs`** + +Instantiates edgezero's `-cli` template (name=`mocktioneer`, struct=`MocktioneerConfig`): + +```rust +//! Mocktioneer CLI — built on the `edgezero-cli` library. +//! +//! Reuses every built-in edgezero command and adds the **typed** `config` +//! arms parameterised over `MocktioneerConfig`, so `validator` rules run on +//! `config validate` / `config push`. + +use clap::{Parser, Subcommand}; +use edgezero_cli::args::{ + AuthArgs, BuildArgs, ConfigPushArgs, ConfigValidateArgs, DeployArgs, ProvisionArgs, ServeArgs, +}; +use mocktioneer_core::config::MocktioneerConfig; + +#[derive(Parser, Debug)] +#[command(name = "mocktioneer-cli", about = "mocktioneer edge CLI")] +struct Args { + #[command(subcommand)] + cmd: Cmd, +} + +#[derive(Subcommand, Debug)] +enum Cmd { + /// Sign in / out / status against the adapter's native CLI. + Auth(AuthArgs), + /// Build the project for a target edge. + Build(BuildArgs), + /// Inspect or mutate the typed `mocktioneer.toml` app config. + #[command(subcommand)] + Config(MocktioneerConfigCmd), + /// Deploy to a target edge. + Deploy(DeployArgs), + /// Create the platform resources backing the declared store ids. + Provision(ProvisionArgs), + /// Run a local simulation (adapter-specific). + Serve(ServeArgs), +} + +/// Dispatches `validate`/`push` to the typed entry points over +/// `MocktioneerConfig`. +#[derive(Subcommand, Debug)] +enum MocktioneerConfigCmd { + /// Push `mocktioneer.toml` (flattened) to the adapter's config store. + Push(ConfigPushArgs), + /// Validate `edgezero.toml` + `mocktioneer.toml` against `MocktioneerConfig`. + Validate(ConfigValidateArgs), +} + +fn main() { + use std::process; + + edgezero_cli::init_cli_logger(); + let result = match Args::parse().cmd { + Cmd::Auth(args) => edgezero_cli::run_auth(&args), + Cmd::Build(args) => edgezero_cli::run_build(&args), + Cmd::Config(MocktioneerConfigCmd::Push(args)) => { + edgezero_cli::run_config_push_typed::(&args) + } + Cmd::Config(MocktioneerConfigCmd::Validate(args)) => { + edgezero_cli::run_config_validate_typed::(&args) + } + Cmd::Deploy(args) => edgezero_cli::run_deploy(&args), + Cmd::Provision(args) => edgezero_cli::run_provision(&args), + Cmd::Serve(args) => edgezero_cli::run_serve(&args), + }; + if let Err(err) = result { + log::error!("[mocktioneer] {err}"); + process::exit(1); + } +} +``` + +- [ ] **Step 4: Build the CLI** + +Run: `cargo build -p mocktioneer-cli` +Expected: PASS. If `run_auth`/`run_provision` names differ, check `edgezero-cli`'s `lib.rs` re-exports and adjust (the generated template uses exactly these names). + +- [ ] **Step 5: Validate the typed config end to end** + +Run: `cargo run -p mocktioneer-cli -- config validate --strict` +Expected: PASS (manifest + `mocktioneer.toml` validate against `MocktioneerConfig`). + +- [ ] **Step 6: Sanity-check rejection** + +Run: `printf 'bid_cpm = -1\n' > /tmp/bad-mocktioneer.toml && cargo run -p mocktioneer-cli -- config validate --strict --app-config /tmp/bad-mocktioneer.toml; echo "exit=$?"` +Expected: non-zero exit with a validation error about `bid_cpm`. + +- [ ] **Step 7: Commit** + +```bash +git add Cargo.toml Cargo.lock crates/mocktioneer-cli +git commit -m "feat: add mocktioneer-cli with typed config validate/push" +``` + +--- + +## Task 9: Dockerfile — keep the dependency-cache layer correct + +**Why (accuracy note):** the build already does `COPY crates ./crates` **before** `cargo fetch --locked` (Dockerfile:21/24), so the workspace fetch resolves regardless of the per-crate manifest pre-copies (lines 16-19). Those pre-copies exist only to create a **cache layer** (manifests change rarely → fetch is cached across source edits). That layer is currently already incomplete — it omits the existing **spin** member. So this task is **cache hygiene**, not a correctness fix: bring the pre-copy list in line with the actual member set. + +**Files:** + +- Modify: `Dockerfile` + +- [ ] **Step 1: Add the missing member manifests to the cache layer** + +In `Dockerfile`, after the existing per-crate manifest COPY lines (after the fastly line, before `COPY crates ./crates`), add the two members currently missing from the pre-copy list: + +```dockerfile +COPY crates/mocktioneer-adapter-spin/Cargo.toml crates/mocktioneer-adapter-spin/Cargo.toml +COPY crates/mocktioneer-cli/Cargo.toml crates/mocktioneer-cli/Cargo.toml +``` + +- [ ] **Step 2: Verify the build (if Docker is available)** + +Run: `docker build -t mocktioneer:plan-check . 2>&1 | tail -20` +Expected: `cargo fetch --locked` resolves and the build completes. (If Docker isn't available, note it and rely on CI; the build is correct either way because `COPY crates` precedes `cargo fetch`.) + +- [ ] **Step 3: Commit** + +```bash +git add Dockerfile +git commit -m "build: copy missing adapter manifests before cargo fetch in Docker" +``` + +--- + +## Task 10: Ignore `.edgezero/` + +**Files:** + +- Modify: `.gitignore` + +- [ ] **Step 1: Add the ignore entry** + +Add `.edgezero/` to `.gitignore` (next to the existing `.spin/` line): + +``` +.edgezero/ +``` + +- [ ] **Step 2: Verify the worktree no longer shows it** + +Run: `git status --porcelain | grep edgezero || echo clean` +Expected: `clean`. + +- [ ] **Step 3: Commit** + +```bash +git add .gitignore +git commit -m "chore: gitignore .edgezero/ (local config/kv state)" +``` + +--- + +## Task 11: Docs, agents, CI-command docs + +**Files:** + +- Modify (Spin wasip1→wasip2): `CLAUDE.md`, `docs/guide/getting-started.md`, `docs/guide/configuration.md`, `.claude/agents/code-architect.md`, `.claude/agents/build-validator.md`, `.claude/agents/verify-app.md`, `.cargo/config.toml` (comment only) +- Modify (pricing default): `docs/guide/what-is-mocktioneer.md`, `docs/guide/architecture.md`, `docs/integrations/prebidjs.md`, `docs/integrations/prebid-server.md`, `docs/integrations/index.md`, `docs/api/openrtb-auction.md`, `docs/api/aps-bid.md`, `docs/api/index.md` +- Modify (CLI story): `README.md`, `docs/guide/adapters/index.md`, `docs/guide/adapters/axum.md`, `docs/guide/adapters/cloudflare.md`, `docs/guide/adapters/fastly.md`, `docs/guide/getting-started.md`, `tests/playwright/README.md`, `tests/playwright/playwright.config.ts` +- Modify (CI gate docs): `.claude/commands/check-ci.md`, `CLAUDE.md` + +- [ ] **Step 1: Spin target references → wasip2** + +In each file listed under "Spin wasip1→wasip2", change Spin-context `wasm32-wasip1` → `wasm32-wasip2`. **Do not** touch Fastly's `wasm32-wasip1` references. Verify scope first: + +Run: `grep -rn "wasip1" CLAUDE.md docs/guide/getting-started.md docs/guide/configuration.md .claude/agents` +For each hit, confirm whether it's Spin (change) or Fastly (leave). In `CLAUDE.md` update the layout comment `Spin / Fermyon bridge (wasm32-wasip1)` and the adapter targets table Spin row to wasip2. + +- [ ] **Step 2: Pricing docs — "$0.20 always/fixed" → "$0.20 default (configurable)"** + +In each pricing doc, reword the fixed-price claim. Example for `docs/api/openrtb-auction.md`: + +> Every bid is priced at a **default** CPM of **$0.20**, configurable via the `bid_cpm` key in `mocktioneer.toml` (pushed to the adapter's config store with `mocktioneer-cli config push`). + +Apply the equivalent one-line edit to each of the 8 files. Verify you caught them: + +Run: `grep -rn "0.20\|fixed price\|always" docs/guide/what-is-mocktioneer.md docs/guide/architecture.md docs/integrations docs/api` + +- [ ] **Step 3: CLI story — distinguish `edgezero-cli` vs `mocktioneer-cli`** + +- `README.md`, `docs/guide/adapters/index.md`, `docs/guide/getting-started.md`: note that `config validate`/`config push` are typed and live in the in-repo `mocktioneer-cli` (`cargo run -p mocktioneer-cli -- …`); `serve`/`build`/`deploy` work from either the external `edgezero-cli` or `mocktioneer-cli`. Drop "optional, not vendored" framing for the config commands. +- **Per-adapter pages** — `docs/guide/adapters/axum.md` (≈L22), `docs/guide/adapters/cloudflare.md` (≈L47), `docs/guide/adapters/fastly.md` (≈L36) each show `edgezero-cli serve/build/deploy` examples. Keep `edgezero-cli` as valid but add a one-line "or, in-repo: `cargo run -p mocktioneer-cli -- `" alongside, so the vendored CLI is mentioned consistently. Find them first: + + Run: `grep -rn "edgezero-cli" docs/guide/adapters/` + +- `tests/playwright/README.md` and `tests/playwright/playwright.config.ts`: change the `webServer` launch command to `cargo run -p mocktioneer-cli -- serve --adapter cloudflare` (no external install). Confirm the exact current command first: + + Run: `grep -n "edgezero-cli\|webServer\|command" tests/playwright/playwright.config.ts` + +- [ ] **Step 4: Add the config-validate gate to local CI docs** + +In `.claude/commands/check-ci.md`, add a step 5: + +```markdown +5. `cargo run -p mocktioneer-cli -- config validate --strict` +``` + +In `CLAUDE.md` "CI Gates" list, add the same `config validate --strict` gate. + +- [ ] **Step 5: Verify docs formatter still passes** + +Run: `cd docs && npm run format` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add CLAUDE.md docs .claude README.md tests/playwright .cargo/config.toml +git commit -m "docs: spin wasip2, bid_cpm-default pricing, edgezero-cli vs mocktioneer-cli, config gate" +``` + +--- + +## Task 12: CI — Spin wasip2 matrix + config-validate gate + +**Files:** + +- Modify: `.github/workflows/test.yml` + +- [ ] **Step 1: Inspect the current Spin matrix entry** + +Run: `grep -n "wasip1\|wasip2\|RUNNER\|target:\|spin" .github/workflows/test.yml` +Identify the Spin matrix row (currently `target: wasm32-wasip1`, `runner_env: CARGO_TARGET_WASM32_WASIP1_RUNNER`). + +- [ ] **Step 2: Switch the Spin matrix row to wasip2** + +For the Spin entry only (leave the Fastly entry on wasip1): change `target: wasm32-wasip1` → `target: wasm32-wasip2` and `runner_env: CARGO_TARGET_WASM32_WASIP1_RUNNER` → `CARGO_TARGET_WASM32_WASIP2_RUNNER`. Ensure the rustup target install step uses the matrix `target` (so it adds `wasm32-wasip2`). Keep the pinned Wasmtime install. + +- [ ] **Step 3: Add the typed-config gate job/step** + +Add a step (in the existing native test job, after `cargo test`): + +```yaml +- name: Validate typed app config + run: cargo run -p mocktioneer-cli -- config validate --strict + +- name: Seed a NON-default cpm and assert it round-trips (axum) + run: | + printf 'bid_cpm = 0.35\n' > /tmp/seed.toml + cargo run -p mocktioneer-cli -- config push --adapter axum --yes --app-config /tmp/seed.toml + # R8 blob model: bid_cpm is inside the envelope under the store key. + test "$(jq -r '.mocktioneer_config | fromjson | .data.bid_cpm' .edgezero/local-config-mocktioneer_config.json)" = "0.35" + rm -f .edgezero/local-config-mocktioneer_config.json +``` + +A **bare** `config push --adapter axum` would silently seed the root `mocktioneer.toml` default (`0.20`) and a `test -f` only proves a file exists — it would pass even if `bid_cpm` were never wired. Seeding `0.35` via `--app-config` and asserting the JSON value with `jq` proves push writes the _configured_ value. The handler → response half (a seeded store yielding `0.35`) is proven deterministically by the registry-backed `auction_uses_seeded_cpm` handler test (R8), so no flaky serve+curl is needed here. (`jq` is preinstalled on GitHub `ubuntu-latest`.) + +- [ ] **Step 4: Lint the workflow locally** + +Prefer `actionlint` (validates GitHub Actions schema, not just YAML): + +Run: `command -v actionlint >/dev/null && actionlint .github/workflows/test.yml || echo "actionlint not installed"` +Expected: PASS, or the not-installed note. + +Fallback YAML well-formedness check **only if PyYAML is available** (`pip install pyyaml`; it is not in the stdlib, so don't rely on it in a clean env): + +Run: `python -c "import importlib.util,sys; sys.exit(0) if importlib.util.find_spec('yaml') is None else __import__('yaml').safe_load(open('.github/workflows/test.yml'))" && echo "yaml-ok-or-skipped"` + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/test.yml +git commit -m "ci: spin wasip2 matrix + typed config validate/seed gate" +``` + +--- + +## Task 13: Full verification pass + +**Files:** none (verification only) + +- [ ] **Step 1: Format + lint** + +Run: `cargo fmt --all -- --check` +Expected: PASS. + +Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` +Expected: PASS. + +- [ ] **Step 2: Tests** + +Run: `cargo test --workspace --all-targets` +Expected: PASS. + +- [ ] **Step 3: Feature/target matrix** + +Run: `cargo check --workspace --all-targets --features "fastly cloudflare"` +Expected: PASS. + +Run: `cargo build -p mocktioneer-adapter-fastly --features fastly --target wasm32-wasip1` +Expected: PASS. + +Run: `cargo build --release -p mocktioneer-adapter-spin --features spin --target wasm32-wasip2` +Expected: PASS. + +Run: `cargo build -p mocktioneer-adapter-cloudflare --features cloudflare --target wasm32-unknown-unknown` +Expected: PASS. + +- [ ] **Step 4: Typed config + seeded read-back** + +Run: `cargo run -p mocktioneer-cli -- config validate --strict` +Expected: PASS. + +Run: `printf 'bid_cpm = 0.35\n' > /tmp/seed.toml && cargo run -p mocktioneer-cli -- config push --adapter axum --yes --app-config /tmp/seed.toml && jq -r '.mocktioneer_config | fromjson | .data.bid_cpm' .edgezero/local-config-mocktioneer_config.json` +Expected: prints `0.35` (R8 blob envelope). (Then remove it: `rm -f .edgezero/local-config-mocktioneer_config.json`.) + +- [ ] **Step 5: Docs gates (incl. spec/plan exclusion assertion)** + +Run: `cd docs && npm run format && npm run lint && npm run build` +Expected: PASS. + +Run: `find docs/.vitepress/dist docs/.vitepress/.temp -path '*superpowers*' -print -quit` +Expected: **no output** (specs/plans excluded from the published build). + +- [ ] **Step 6: Semantic parity vs `main` (R8: seed config first)** + +Under R8 the auction/APS endpoints are fail-loud, so seed first: +`cargo run -p mocktioneer-cli -- config push --adapter axum --yes`. Then (via +`cargo run -p mocktioneer-adapter-axum`) hit `/openrtb2/auction` and +`/e/dtb/bid` and confirm prices are `0.20` and the responses match `main` on +semantic fields (price, sizes, `cur`, creative URLs, targeting) — IDs differ by +design (`Uuid::now_v7()`). Without a pushed blob both endpoints return +`config_out_of_date` (expected). + +- [ ] **Step 7: Final commit (if any verification fixups were needed)** + +```bash +git add -A +git commit -m "chore: verification fixups for edgezero #269 adaptation" +``` + +--- + +## Self-Review (plan vs spec) + +- **§3.1 deps/anyhow/clap/spin-sdk** → Task 1. ✓ +- **§3.2 adapter entrypoints** → Tasks 2 (axum/cf/fastly) + 3 (spin). ✓ +- **§3.3 Spin wasip2 + runner** → Task 3 (adapter) + Task 12 (CI runner env). ✓ +- **§3.4 manifest `[stores.config]`** → Task 4. ✓ +- **§3.5 typed struct + runtime contract** → Task 5 (struct) + Task 6 (R8: fail-loud `AppConfig` extractor; no pushed blob → error; invalid value → validation error). ✓ +- **§3.6 config lifecycle (per-adapter backing, spin runtime-config)** → Task 3 (spin runtime-config.toml) + Task 4 (commands) + Task 8 (push) + Task 12 (axum seed). Cloud/Spin provision documented, not CI-stood-up, per spec. ✓ +- **§3.7 mocktioneer-cli (metadata/lints) + Dockerfile** → Task 8 + Task 9. ✓ +- **§3.8 docs (wasip2, pricing default, CLI story, gitignore, check-ci, prettier/VitePress)** → Task 0 (prettier/VitePress) + Task 10 (gitignore) + Task 11 (rest). ✓ +- **§3.9 CI** → Task 12. ✓ +- **§5 verification (incl. explicit seeded fixture, docker, prettier)** → Task 13 (+ R8 registry-backed handler tests in Task 6 covering seeded-0.35 and no-config-error; non-default `0.35` seed + `jq` envelope assert in Tasks 12/13; docs build-exclusion `find` check in Tasks 0/13). ✓ + +No placeholders; types/functions (`MocktioneerConfig`, the `AppConfig` extractor, builder signatures with `cpm: f64`) are consistent across tasks (R8). + +--- + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-06-15-edgezero-extensible-cli-adaptation.md`. Two execution options: + +**1. Subagent-Driven (recommended)** — a fresh subagent per task, two-stage review between tasks, fast iteration. + +**2. Inline Execution** — execute tasks in this session with batch checkpoints (executing-plans). + +Which approach? diff --git a/docs/superpowers/specs/2026-06-11-edgezero-extensible-cli-adaptation-design.md b/docs/superpowers/specs/2026-06-11-edgezero-extensible-cli-adaptation-design.md new file mode 100644 index 0000000..a42e333 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-edgezero-extensible-cli-adaptation-design.md @@ -0,0 +1,460 @@ +# Design: Adapt Mocktioneer to edgezero #269 (extensible CLI) + +- **Date:** 2026-06-11 +- **Branch:** `feature/edgezero-extensible-cli` (off `main`) +- **Upstream:** [stackpop/edgezero#269](https://github.com/stackpop/edgezero/pull/269) + — "EdgeZero CLI Extensions". Head `feature/extensible-cli`, base **`main`** + (rebased off `chore/strict-clippy`), **OPEN / unmerged** (re-verified via + `gh pr view 269` on 2026-06-15: state OPEN, base `main`, updated + 2026-06-12; re-check at implementation). +- **Scope (approved):** Compat + typed config, with `bid_cpm` consumed at + runtime. Pin deps to `feature/extensible-cli` now; re-pin to `main` after + #269 merges. +- **Revision history:** R1 initial; R2 review (config lifecycle, validators, + scope); **R3 second review** — corrects two errors I made: (a) the **APS + path also uses the fixed CPM** (`build_aps_response` lives in `auction.rs` + and reads `FIXED_BID_CPM`), so `handle_aps_bid` is in scope; (b) Axum binds + an **empty** store when the local file is missing, so `Ok(None)` must + **fall back**, not error. Also: Spin config is KV-backed; `EdgeError`/ + validator signatures corrected; no `.tool-versions` bump needed; gitignore + + check-ci updates added. **R4 base change** — rebased onto `main`: PR #108 + ("Adopt edgezero strict-clippy gate and PR #257 API") merged the prerequisite + strict-clippy/#257 adaptation into `main`, and `main` pins all `edgezero-*` + deps at `branch = "main"`, so the dep repin is now `main` → + `feature/extensible-cli` (no longer stacked on `chore/edgezero-strict-clippy`). + **R5** — folded all prior review findings into the body: anyhow-in-core + (§3.1), Dockerfile manifest COPY (§3.7/§5), cli-crate metadata/lints (§3.7), + mandatory docs-formatter + VitePress exclusion (§3.8), the wider + edgezero-cli→mocktioneer-cli story incl. README/Playwright (§3.8), the public + "$0.20 always" → "$0.20 default" pricing-doc rewrite (§3.8), the Spin + `runtime-config.toml`/`--runtime-config-file` requirement (§3.6), the + malformed-config-FILE-vs-read-error nuance (§3.5), and an explicit seeded + test fixture (§5). **R6 (PR #110 review)** — CI seed gate now seeds a + **non-default** `0.35` and `jq`-asserts it (a bare push silently seeds the + `0.20` default); the §5 runtime fixture is a **registry-backed + `resolve_bid_cpm`** test (public `StoreRegistry`/`ConfigStoreHandle`, app-demo + `config_flow.rs` pattern), not just the pure parser; Dockerfile reframed as + cache hygiene (+ add the missing spin manifest); CLI-story widened to the + per-adapter doc pages; docs `srcExclude` made verifiable + `.vitepress/.temp` + ignored; plan call-site note corrected (mediation has a _separate_ + `build_openrtb_response`). **R7 (during implementation)** — dropped the `new` + subcommand from `mocktioneer-cli`: scaffolding a brand-new EdgeZero app from + within Mocktioneer's own project CLI is nonsensical, so the crate exposes + `auth`/`build`/`deploy`/`provision`/`serve` + typed `config` only. + **R8 (sync to edgezero `89f59266`)** — supersedes the §3.5 read model. edgezero + advanced past the pin with a **blob app-config cutover**: the whole typed + config is stored as one canonical-JSON **blob envelope** (SHA-gated) under the + store's key, read via the new **`AppConfig` extractor**, not per-leaf + `get("bid_cpm")`. Per the user's call, the handlers now use the **fail-loud** + bare `AppConfig(cfg): AppConfig` extractor (dropping the + graceful `resolve_bid_cpm` wrapper): OpenRTB/APS **require a `config push`** + before serving (they error otherwise; `FIXED_BID_CPM` is the builder default / + shipped value, no longer a runtime fallback). Also: added the new `config diff` + command (`DiffExit`) to `mocktioneer-cli`; `config push` now needs `--yes` in + CI; the local-config file is `{ "mocktioneer_config": "" }`, so the + CI assertion is `jq -r '.mocktioneer_config | fromjson | .data.bid_cpm'`; and + endpoint/handler tests seed a blob via a `ConfigRegistry` fixture + (`StoreRegistry` + `ConfigStoreBinding` + `BlobEnvelope::new`). + +## 1. Problem & context + +Mocktioneer consumes six `edgezero-*` crates from git, pinned (on `main`) to +edgezero's `branch = "main"`. PR #269 is a large, intentionally +backward-incompatible refactor. We adapt to the new APIs ahead of merge and +adopt the typed app-config surface, with `bid_cpm` wired through at request +time. (Base note: `main` already carries the strict-clippy + PR #257 edgezero +adaptation via PR #108, so this branch starts from that merged state.) + +### What #269 changes that actually reaches this repo + +From the sibling checkout `../edgezero` (`feature/extensible-cli`): + +- Mocktioneer has **no `[stores.*]`** today and makes **no + `kv_store`/`config_store`/`secret_store` calls**, so the store breakages are + inert until we opt in. +- `ctx.path()?` (with a `let x: T =` annotation), the `RequestContext(ctx)` + destructure, `RequestContext::new`, `ctx.request()`, and + `edgezero_core::context::RequestContext` **all still compile**. +- `app!` **still accepts the 2-arg custom-name form** → `MocktioneerApp` + retained, **no app rename**. +- `[adapters.*.build]`/`[adapters.*.commands]` remain valid; only legacy + `[stores.*]` and `[adapters.*.stores]` shapes are rejected — neither present. + +Parts that **do** reach this repo: + +1. Every adapter's `run_app` **dropped the `include_str!(manifest)` arg**. +2. **Spin → `spin-sdk ~6.0` / `wasm32-wasip2`** (`#[http_service]`). Fastly + stays `wasm32-wasip1`. +3. New **typed `AppConfig`** + generated `-cli` shape + per-adapter + config-store **lifecycle** (declare → seed → auto-bind on serve → read). + +## 2. Goals / non-goals + +**Goals** + +- Builds, lints (`-D warnings`), tests green against `feature/extensible-cli` + on all targets (native, Fastly `wasm32-wasip1`, Spin `wasm32-wasip2`, + Cloudflare `wasm32-unknown-unknown`). +- Typed config: validated `MocktioneerConfig`, `mocktioneer.toml`, + `mocktioneer-cli`, and a **required** `config validate --strict` CI gate. +- `bid_cpm` consumed at runtime by **both** the OpenRTB auction path + (`handle_openrtb_auction`) **and** the APS path (`handle_aps_bid`), since + both currently emit `FIXED_BID_CPM`, read via the typed config (R8: fail-loud + `AppConfig` extractor — a `config push` is required before serving). + +**Non-goals** + +- KV or secret stores. +- Re-pinning to `main` (post-merge follow-up). +- Threading any other config field through rendering/business logic (config v1 + is `bid_cpm` only). +- Fixing pre-existing `Uuid::now_v7()` non-determinism in bid IDs (§5; out of + scope). + +## 3. Design + +### 3.1 Dependency pin — `Cargo.toml` + +- Six `edgezero-*` git deps: `branch = "main"` → `branch = "feature/extensible-cli"`. +- `spin-sdk = "5.2"` → `{ version = "~6.0", default-features = false, +features = ["http", "key-value", "variables"] }`. +- Add `clap = { version = "4", features = ["derive"] }` to + `[workspace.dependencies]` (consumed as `clap = { workspace = true }`). +- Add `anyhow = { workspace = true }` to **`crates/mocktioneer-core/Cargo.toml`** + (the §3.5 helper uses `anyhow::anyhow!`; core does not currently depend on + anyhow — only the workspace table does). `anyhow` is WASM-compatible. +- Regenerate `Cargo.lock`. + +### 3.2 Adapter entrypoints — drop the manifest arg + +| File | After | +| ------------------------------------------- | ---------------------------------------------------------------- | +| `mocktioneer-adapter-axum/src/main.rs` | `run_app::()` | +| `mocktioneer-adapter-cloudflare/src/lib.rs` | `run_app::(req, env, ctx)` | +| `mocktioneer-adapter-fastly/src/main.rs` | `run_app::(req)` | +| `mocktioneer-adapter-spin/src/lib.rs` | `#[http_service]` + `Request` + `run_app::(req)` | + +Spin `lib.rs` swaps `expect(unsafe_code, …)` → `allow(unsafe_code, reason = +"spin's #[http_service] macro generates the unsafe wasm export")`; final +import/`no_main` shape follows the SDK-6 template (compile-driven). + +### 3.3 Spin → `wasm32-wasip2` + +- `crates/mocktioneer-adapter-spin/spin.toml`: `source` path + `build.command` + `wasip1` → `wasip2`. +- `edgezero.toml` `[adapters.spin.build].target`: `wasip1` → `wasip2`. +- `crates/mocktioneer-adapter-spin/src/main.rs` host-stub messages. +- `crates/mocktioneer-adapter-spin/tests/contract.rs` comments + runner + invocation (now a `wasm32-wasip2` component), mirroring edgezero's spin + contract test. +- **Fastly unchanged** (`wasm32-wasip1`). + +**`.tool-versions` — no change needed (verified).** Mocktioneer's +`.tool-versions` has no Spin pin; the only deltas vs edgezero are Fastly +(13.0.0 vs 15.1.0) and Wasmtime (45.0.0 vs 44.0.1). Mocktioneer's +**Wasmtime 45.0.0 already supports wasip2/component-model** (newer than +edgezero's pin), so running wasip2 Spin components needs no bump. Confirm at +implementation that 45.0.0 runs the component; only then revisit. + +**Runner config:** `.cargo/config.toml` defines only `[target.wasm32-wasip1]` +(Viceroy, Fastly). Spin wasip2 contract tests use +`CARGO_TARGET_WASM32_WASIP2_RUNNER` (e.g. `wasmtime run`) set **per-job in +CI** and documented for local runs — **not** a global config entry that would +disturb Fastly's wasip1 Viceroy runner. + +### 3.4 Manifest — `edgezero.toml` + +- Existing `[[triggers.http]]` / `[adapters.*]` tables accepted as-is. +- Add: + ```toml + [stores.config] + ids = ["mocktioneer_config"] + ``` +- Add per-adapter native-backing entries where the new schema requires them + for non-axum adapters (see §3.6; compile/validate-driven). +- `manifest.validate()` runs at compile time inside `app!`. + +### 3.5 Typed config struct + runtime contract + +**Struct** — `crates/mocktioneer-core/src/config.rs`: + +```rust +use serde::{Deserialize, Serialize}; +use validator::Validate; + +#[derive(Debug, Deserialize, Serialize, Validate, edgezero_core::AppConfig)] +#[serde(deny_unknown_fields)] +pub struct MocktioneerConfig { + /// Fixed bid CPM in USD. Strictly positive — `exclusive_min` rejects + /// `0.0`, negatives, and `NaN`; non-finite floats are also rejected by + /// edgezero's typed loader. (`_f64` suffix satisfies the strict + /// `default_numeric_fallback` clippy lint.) + #[validate(range(exclusive_min = 0.0_f64))] + pub bid_cpm: f64, +} +``` + +- `creative_label` (R1 draft) removed — YAGNI/unused. +- `pub mod config;` in `crates/mocktioneer-core/src/lib.rs`. +- Root **`mocktioneer.toml`** (1:1, no `[config]` wrapper): `bid_cpm = 0.20`. + +**Runtime resolution — `AppConfig` extractor, fail-loud (R8 / blob model).** + +> Superseded the original per-leaf `resolve_bid_cpm`/`get("bid_cpm")` design. +> edgezero `89f59266` stores the whole typed config as one canonical-JSON +> **blob envelope** (SHA-gated) under the store's key, read via the +> `AppConfig` extractor — there is no per-key `get("bid_cpm")`. + +`auction.rs`'s `build_openrtb_response` and `build_aps_response` take a +`cpm: f64` parameter (replacing direct `FIXED_BID_CPM` reads). The two handlers +read the typed config with the **bare, fail-loud extractor** and pass +`cfg.bid_cpm` in: + +```rust +// routes.rs +use edgezero_core::extractor::AppConfig; +use crate::config::MocktioneerConfig; + +#[action] +pub async fn handle_openrtb_auction( + RequestContext(ctx): RequestContext, + ForwardedHost(host): ForwardedHost, + ValidatedJson(req): ValidatedJson, + AppConfig(cfg): AppConfig, +) -> Result { + // ... signature handling ... + let resp = build_openrtb_response(&req, &host, signature_status, cfg.bid_cpm); +} + +#[action] +pub async fn handle_aps_bid( + ForwardedHost(host): ForwardedHost, + ValidatedJson(req): ValidatedJson, + AppConfig(cfg): AppConfig, +) -> Result { + let resp = build_aps_response(&req, &host, cfg.bid_cpm); +} +``` + +**Contract (fail-loud — the approved trade-off):** the `AppConfig` extractor +fetches the blob at the bound store's `default_key`, verifies the envelope SHA, +deserialises into `MocktioneerConfig`, and runs `validator`. Outcomes: + +| Runtime situation | Result | +| -------------------------------------------------------- | ---------------------------------------------------------------------- | +| No `[stores.config]` / no config store bound | **error** (`EdgeError::internal` "no default config store registered") | +| Store bound but **no blob pushed** yet | **error** (`config_out_of_date` — "run `config push`") | +| Blob present, valid | typed `cfg.bid_cpm` | +| Blob present, value invalid (`bid_cpm` ≤ 0 / non-finite) | **error** (validation) | + +So OpenRTB/APS **require a `config push` per deploy** before they serve; +`FIXED_BID_CPM` is the builders' default arg + the shipped `mocktioneer.toml` +value, **not** a runtime fallback. The static/creative/pixel endpoints don't +use the extractor and are unaffected. (The earlier graceful-fallback wrapper +`resolve_bid_cpm` was dropped per the user's R8 decision.) + +### 3.6 Config lifecycle (declare → seed → bind → read) + +Binding on the **serve path is automatic** once the store is declared and the +backing exists — `edgezero-adapter-axum/src/dev_server.rs:333 run_app` → +`build_config_registry(stores.config)` reads +`.edgezero/local-config-.json`; Cloudflare (`request.rs:362`), Fastly +(`request.rs:382`), Spin (`config_store.rs` via `request::build_config_registry`) +have equivalents. The `app!` macro emits `stores().config` from +`[stores.config]`. Axum binds an empty store when the file is missing (it does +not skip the id); under the R8 fail-loud model the `AppConfig` extractor then +errors `config_out_of_date` (blob absent) — so the store must be **seeded with +a `config push`** before the auction/APS routes serve. + +Per-adapter backing + seed step: + +| Adapter | Backing for `mocktioneer_config` | Seed | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| **Axum** (primary local/CI path) | `.edgezero/local-config-mocktioneer_config.json` | `mocktioneer-cli config push --adapter axum` writes it. | +| **Cloudflare** | KV namespace (config moved `[vars]`→KV in #269) | `provision --adapter cloudflare` (writes id to `wrangler.toml`) + `config push --adapter cloudflare`. | +| **Fastly** | config store + `[setup]`/`[local_server]` in `fastly.toml` | `provision --adapter fastly` + `config push --adapter fastly`. | +| **Spin** | **KV-backed** (`key_value_stores = ["mocktioneer_config"]` in `spin.toml` + a **`runtime-config.toml`** declaring the `[key_value_store.mocktioneer_config]` backend) — config is KV-backed in #269; `[variables]` is secrets-only now (`edgezero-adapter-spin/src/config_store.rs`). | `provision --adapter spin` + `config push --adapter spin`. | + +**Spin runtime-config gap (must add):** Mocktioneer's Spin adapter has only +`spin.toml` and **no `runtime-config.toml`**, and `edgezero.toml`'s spin +commands (`spin up/build/deploy --from …/spin.toml`) pass **no +`--runtime-config-file`**. KV-backed config needs both: add +`crates/mocktioneer-adapter-spin/runtime-config.toml` with a +`[key_value_store.mocktioneer_config]` entry, and update the +`[adapters.spin.commands]` `serve`/`deploy` lines (and docs) to pass +`--runtime-config-file crates/mocktioneer-adapter-spin/runtime-config.toml`. + +**Pragmatics:** the **axum** path is the one exercised locally and in CI. +Cloud/Spin adapters get the manifest declaration + required native-backing +tables (incl. the Spin `runtime-config.toml`) so they _build and validate_; +push/provision are documented but live cloud/Spin stores are not stood up in +CI. Handler tests wire a `ConfigRegistry` fixture directly (the app-demo +`handlers.rs` test pattern) to cover the seeded-value, no-config (error), +and malformed-value (error) branches without a live backend. + +### 3.7 `mocktioneer-cli` crate + +`crates/mocktioneer-cli/`, mirroring `edgezero-cli/src/templates/cli/`: + +- `Cargo.toml`: deps `mocktioneer-core`, `edgezero-cli`, + `clap = { workspace = true }`, `log`. **Package metadata matching the other + crates:** `publish = false`, `license.workspace = true`, and + `[lints] workspace = true` (cf. `mocktioneer-core/Cargo.toml`). +- `src/main.rs`: clap `Args`/`Cmd` flattening + `edgezero_cli::run_{auth,build,deploy,provision,serve}` + a typed + `Config` subcommand dispatching `run_config_validate_typed`, + `run_config_push_typed`, and (R8) `run_config_diff_typed::` + (the new `config diff` command — returns `DiffExit`; non-zero codes + `process::exit`, all errors exit `2`). +- Add `crates/mocktioneer-cli` to root `[workspace].members`. +- **Dockerfile (cache hygiene, not a correctness fix):** the build already does + `COPY crates ./crates` **before** `cargo fetch --locked` (Dockerfile:21/24), + so the workspace fetch resolves regardless of the per-crate manifest + pre-copies (Dockerfile:14-19). Those pre-copies exist only to create a + dependency **cache layer**, and that layer is already incomplete — it omits + the existing **spin** member. Bring it in line by adding the two missing + member manifests: + `COPY crates/mocktioneer-adapter-spin/Cargo.toml …` and + `COPY crates/mocktioneer-cli/Cargo.toml …`. (The image still ships only the + axum binary.) + +### 3.8 Docs, agents, ignore files (verified surface) + +- **Spin wasip1 → wasip2** + spin-sdk-6 note, across the files that reference + it (rg-verified): `CLAUDE.md`, `edgezero.toml`, the spin crate files (§3.3), + `.claude/agents/{code-architect,build-validator,verify-app}.md`, + `docs/guide/getting-started.md`, `docs/guide/configuration.md`, + `.cargo/config.toml` (comment), `.github/workflows/test.yml` (§3.9). + **Leave Fastly wasip1 intact.** No `docs/guide/adapters/spin.md` exists. +- **Pricing docs — "$0.20 always/fixed" → "$0.20 default (configurable via + `bid_cpm`)".** This change makes the fixed CPM a _default_, not an invariant, + so the public claims must be reworded: `docs/guide/what-is-mocktioneer.md`, + `docs/guide/architecture.md`, `docs/integrations/prebidjs.md`, + `docs/integrations/prebid-server.md`, `docs/integrations/index.md`, + `docs/api/openrtb-auction.md`, `docs/api/aps-bid.md`, `docs/api/index.md` + (rg-verified). Mention the config + push path briefly; keep `0.20` as the + shipped default. +- **edgezero-cli → mocktioneer-cli story (wider than VitePress docs).** After + adding the in-repo `mocktioneer-cli`, distinguish the two everywhere they're + referenced: `README.md`, `docs/guide/getting-started.md`, + `docs/guide/adapters/index.md`, the **per-adapter pages** that show + `edgezero-cli` examples — `docs/guide/adapters/axum.md`, + `docs/guide/adapters/cloudflare.md`, `docs/guide/adapters/fastly.md` (keep + `edgezero-cli` valid, add the in-repo `mocktioneer-cli` alternative + alongside) — `tests/playwright/README.md`, and + `tests/playwright/playwright.config.ts` (the `webServer` command). Rule: + **`config validate`/`config push` are typed and live only in + `mocktioneer-cli`**; `serve`/`build`/`deploy`/`auth`/`provision` work from + either the external `edgezero-cli` or the vendored `mocktioneer-cli`. Update + the "optional, not vendored" framing — `mocktioneer-cli` _is_ in-repo. + Playwright's `webServer` should use `cargo run -p mocktioneer-cli -- serve +--adapter …` (no external install needed). +- **`.gitignore`:** add `.edgezero/` (currently only `.spin/` is ignored; the + worktree already has an untracked `.edgezero/`). +- **`.claude/commands/check-ci.md`** and **`CLAUDE.md` "CI Gates"**: add the + new `config validate --strict` gate (exact command in §3.9) so local CI docs + aren't stale. +- **Docs formatter + VitePress exclusion (mandatory — this spec lives under + `docs/`).** `docs/package.json`'s `format` runs `prettier --check .` and the + format CI job runs it; it **fails on this spec file** today, and `npm run +build` renders specs/plans into `docs/.vitepress/dist/…/superpowers/…` (and a + `.vitepress/.temp/`). Required: (a) add `superpowers/` to + `docs/.prettierignore`; (b) add `srcExclude: ['**/superpowers/**']` to the + VitePress config so internal specs aren't published; (c) ignore the build + temp dir in **all three** tools — `.vitepress/.temp` in `docs/.prettierignore` + AND `.vitepress/.temp/**` in `docs/eslint.config.js` `ignores` (ESLint's flat + config has its own ignore list; a `.gitignore`/`.prettierignore` entry does + not stop ESLint scanning it) AND `.vitepress/.temp` in `docs/.gitignore`; + (d) **verify after a build, not before** — run `npm run build` first, then + `npm run format && npm run lint`, and assert + `find docs/.vitepress/dist docs/.vitepress/.temp -path '*superpowers*' -print +-quit` yields nothing. (The prior revision only checked `format` pre-build, so + it missed the ~700 ESLint errors generated files produce.) + +### 3.9 CI — `.github/workflows/test.yml` + +- Spin matrix: target `wasip1` → `wasip2`; install the `wasm32-wasip2` rustup + target; set `CARGO_TARGET_WASM32_WASIP2_RUNNER`; keep the pinned Wasmtime + install and confirm it runs wasip2 components. +- `mocktioneer-cli` covered by `--workspace`. +- **Required gate** (also mirror `config validate --strict` into + `.claude/commands/check-ci.md` and the `CLAUDE.md` CI-gates list). Seed a + **non-default** value so the gate actually proves `bid_cpm` is wired — a bare + `config push --adapter axum` would seed the root default (`0.20`) and a + `test -f` would pass even if nothing were wired: + ```sh + cargo run -p mocktioneer-cli -- config validate --strict + printf 'bid_cpm = 0.35\n' > /tmp/seed.toml + cargo run -p mocktioneer-cli -- config push --adapter axum --yes --app-config /tmp/seed.toml + # R8 blob model: bid_cpm lives inside the envelope under the store key. + test "$(jq -r '.mocktioneer_config | fromjson | .data.bid_cpm' .edgezero/local-config-mocktioneer_config.json)" = "0.35" + ``` + The handler → response half (seeded store → `0.35`) is proven deterministically + by the registry-backed handler-dispatch test (§5, `auction_uses_seeded_cpm`), + so no flaky serve+curl is needed in CI. +- **Docker:** `.github/workflows/docker.yml` builds the image. The Dockerfile + change (§3.7) is cache hygiene only — `COPY crates` already precedes + `cargo fetch`, so the build resolves regardless. A `docker build` smoke is a + nice-to-have, not a gate. +- **Docs formatter is mandatory, not conditional:** the format CI job already + fails on this spec, so the `docs/.prettierignore` + VitePress `srcExclude` + changes (§3.8) must land in this branch. + +## 4. Risks & mitigations + +| Risk | Mitigation | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| New parser rejects an existing `[adapters.*]` table | Compile-time `manifest.validate()` surfaces it; fix per upstream error. | +| Spin SDK 6 macro/type churn beyond template | Mirror edgezero's `edgezero-adapter-spin` verbatim; build wasip2. | +| Wasmtime can't run the wasip2 component | Wasmtime 45.0.0 supports it; set `CARGO_TARGET_WASM32_WASIP2_RUNNER`; match edgezero's contract-test config. | +| Fresh dev errors before any push (R8 fail-loud) | Intended: auction/APS require `config push` once per deploy; documented in `configuration.md`/README (§3.5). | +| Broken/malformed pushed _value_ masked as $0.20 | Read errors propagate; malformed present value errors (§3.5). Note a malformed _file_ degrades to fallback (bind-time drop), by design. | +| `bid_cpm` never exercised (store unseeded) | CI seeds a non-default `0.35` via `--app-config` and `jq`-asserts it round-trips; registry-backed `auction_uses_seeded_cpm` handler test covers the read path (§5). | +| Spin KV config silently empty (no `runtime-config.toml`) | Add `runtime-config.toml` + `--runtime-config-file` to spin commands (§3.6). | +| Docker dependency-cache layer stale/incomplete | Cache hygiene only — `COPY crates` precedes `cargo fetch`; add the missing spin + cli manifests to the pre-copy list (§3.7). | +| Spec under `docs/` fails the format CI gate | Mandatory `docs/.prettierignore` + VitePress `srcExclude` (§3.8). | +| Pinning to an unmerged branch | Documented; re-pin to edgezero `main` post-merge. | +| `.cargo/config.toml.local` patch drift (`edgezero-macros`) | Already lists it; verify it patches cleanly. | + +## 5. Verification + +1. `cargo fmt --all -- --check` +2. `cargo clippy --workspace --all-targets --all-features -- -D warnings` +3. `cargo test --workspace --all-targets` +4. `cargo check --workspace --all-targets --features "fastly cloudflare"` +5. Wasm builds: Fastly (`wasm32-wasip1`), Spin (`wasm32-wasip2`), Cloudflare + (`wasm32-unknown-unknown`). +6. `cargo run -p mocktioneer-cli -- config validate --strict` passes; negative + / non-finite `bid_cpm` fail validation. +7. Spin `contract.rs` green under the wasip2 wasmtime runner. +8. **Semantic** comparison of OpenRTB **and APS** responses vs `main` with no + config store bound: bid/slot `price` (and APS `encoded_price`), sizes, + `cur`, creative markup/URLs, targeting match; volatile fields (IDs from + `Uuid::now_v7()`, auction.rs:51 / mediation.rs:102 / APS `new_id()`) are + excluded — byte equality is impossible even on `main`. +9. Runtime exercise — **explicit fixtures** (the root `mocktioneer.toml` ships + `bid_cpm = 0.20`, so 0.35 must come from a seeded store, not the default): + - **Registry-backed handler unit (R8):** build a `RequestContext` with a + `ConfigRegistry` whose default store holds a **blob envelope** for + `{ "bid_cpm": 0.35 }` (`StoreRegistry::single_id` + `ConfigStoreBinding` + + `BlobEnvelope::new` over an in-memory `MapConfigStore`, inserted via + `request.extensions_mut().insert(registry)` — the app-demo + `config_flow.rs` pattern). Dispatch `handle_openrtb_auction` and assert the + bid `price` is `0.35`; dispatch with **no** registry and assert the handler + **errors** (fail-loud). The `0.20` default `ctx()` seeds the same way. + - **CI seed path (axum):** `printf 'bid_cpm = 0.35\n' > /tmp/seed.toml` then + `config push --adapter axum --yes --app-config /tmp/seed.toml`, and assert + `jq -r '.mocktioneer_config | fromjson | .data.bid_cpm' …` == `0.35` + (the blob envelope; not a bare push + `test -f`). +10. `docs/` gates pass: `cd docs && npm run format && npm run lint && npm run +build`, **and** `find docs/.vitepress/dist docs/.vitepress/.temp -path +'*superpowers*' -print -quit` produces no output (specs/plans excluded). +11. `docker build` succeeds with `mocktioneer-cli` in the workspace + (nice-to-have; not a gate — the build resolves regardless per §3.7). + +## 6. Rollback / follow-ups + +- Rollback = revert the branch; deps return to edgezero `branch = "main"`. +- Follow-up: re-pin all `edgezero-*` deps to `main` once #269 merges. +- Pre-existing tech-debt (not fixed here): bid IDs use `Uuid::now_v7()`, which + is non-deterministic despite the project's determinism guarantee. diff --git a/edgezero.toml b/edgezero.toml index e97654d..29d1a39 100644 --- a/edgezero.toml +++ b/edgezero.toml @@ -6,6 +6,33 @@ middleware = [ "mocktioneer_core::routes::Cors" ] +[stores.config] +ids = ["mocktioneer_config"] +default = "mocktioneer_config" + +# Introspection routes (framework-supplied by edgezero_core). Unauthenticated — +# they emit the manifest, effective config (secret-safe), and route table. +[[triggers.http]] +id = "introspect_manifest" +path = "/_mocktioneer/manifest" +methods = ["GET"] +handler = "edgezero_core::introspection::manifest" +adapters = ["axum", "cloudflare", "fastly", "spin"] + +[[triggers.http]] +id = "introspect_config" +path = "/_mocktioneer/config" +methods = ["GET"] +handler = "edgezero_core::introspection::config" +adapters = ["axum", "cloudflare", "fastly", "spin"] + +[[triggers.http]] +id = "introspect_routes" +path = "/_mocktioneer/routes" +methods = ["GET"] +handler = "edgezero_core::introspection::routes" +adapters = ["axum", "cloudflare", "fastly", "spin"] + [[triggers.http]] id = "root" path = "/" @@ -249,14 +276,14 @@ crate = "crates/mocktioneer-adapter-spin" manifest = "crates/mocktioneer-adapter-spin/spin.toml" [adapters.spin.build] -target = "wasm32-wasip1" +target = "wasm32-wasip2" profile = "release" features = ["spin"] [adapters.spin.commands] build = "spin build --from crates/mocktioneer-adapter-spin/spin.toml" deploy = "spin deploy --from crates/mocktioneer-adapter-spin/spin.toml" -serve = "spin up --from crates/mocktioneer-adapter-spin/spin.toml" +serve = "spin up --from crates/mocktioneer-adapter-spin/spin.toml --runtime-config-file crates/mocktioneer-adapter-spin/runtime-config.toml" [adapters.spin.logging] level = "info" diff --git a/mocktioneer.toml.example b/mocktioneer.toml.example new file mode 100644 index 0000000..457971e --- /dev/null +++ b/mocktioneer.toml.example @@ -0,0 +1,6 @@ +# Typed application config for Mocktioneer (maps 1:1 onto MocktioneerConfig). +# `bid_cpm` is the shipped default. To change it per-environment, set +# `MOCKTIONEER__BID_CPM` (the env overlay is applied when the CLI loads this +# file) and/or edit it, then `config push` — the overlay affects the value the +# CLI pushes into the config-store blob, NOT already-pushed runtime config. +bid_cpm = 0.20 diff --git a/tests/playwright/README.md b/tests/playwright/README.md index cd7a834..7a766ee 100644 --- a/tests/playwright/README.md +++ b/tests/playwright/README.md @@ -9,11 +9,9 @@ npm install npx playwright install ``` -**Note:** The Cloudflare adapter requires `edgezero-cli` (not `edgezero`): - -```bash -cargo install --git https://github.com/stackpop/edgezero.git edgezero-cli -``` +**Note:** The Cloudflare web server is launched via the in-repo +`mocktioneer-cli` (`cargo run -p mocktioneer-cli -- serve --adapter cloudflare`), +so no external `edgezero-cli` install is required. ## Running Tests @@ -45,9 +43,9 @@ npx playwright show-report The `ADAPTER` environment variable controls which adapter is tested: -| Value | Command | Description | -| ---------------- | ----------------------------------------- | ------------------------- | -| `axum` (default) | `cargo run -p mocktioneer-adapter-axum` | Native Axum server | -| `cloudflare` | `edgezero-cli serve --adapter cloudflare` | Cloudflare Workers (WASM) | +| Value | Command | Description | +| ---------------- | ------------------------------------------------------ | ------------------------- | +| `axum` (default) | `cargo run -p mocktioneer-adapter-axum` | Native Axum server | +| `cloudflare` | `cargo run -p mocktioneer-cli -- serve --adapter cloudflare` | Cloudflare Workers (WASM) | Both adapters run on `http://127.0.0.1:8787`. diff --git a/tests/playwright/playwright.config.ts b/tests/playwright/playwright.config.ts index aecfb1f..a7b0140 100644 --- a/tests/playwright/playwright.config.ts +++ b/tests/playwright/playwright.config.ts @@ -8,7 +8,8 @@ const adapter = process.env.ADAPTER || 'axum'; const webServerCommands: Record = { axum: 'cargo run -p mocktioneer-adapter-axum', - cloudflare: 'edgezero-cli serve --adapter cloudflare', + // In-repo CLI — no external `edgezero-cli` install needed. + cloudflare: 'cargo run -p mocktioneer-cli -- serve --adapter cloudflare', }; export default defineConfig({