diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4b963e0153..45ea7f8a5e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,6 +19,16 @@ on: schedule: - cron: "42 6 * * 0" +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: analyze: name: Analyze (${{ matrix.language }}) @@ -28,6 +38,10 @@ jobs: # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-analyze-${{ matrix.language }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max permissions: # required for all workflows security-events: write diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 5419097d86..5a00f839e2 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -8,10 +8,24 @@ on: permissions: contents: read +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: check-claude-md-symlink: name: CLAUDE.md symlink guard runs-on: ubuntu-latest + timeout-minutes: 5 + concurrency: + group: ${{ github.workflow }}-check-claude-md-symlink-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -42,6 +56,10 @@ jobs: check-ci-node-pin: name: CI Node pin guard runs-on: ubuntu-latest + timeout-minutes: 5 + concurrency: + group: ${{ github.workflow }}-check-ci-node-pin-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -66,6 +84,10 @@ jobs: format-rust: name: cargo fmt runs-on: ubuntu-latest + timeout-minutes: 25 + concurrency: + group: ${{ github.workflow }}-format-rust-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -97,6 +119,25 @@ jobs: cache: npm cache-dependency-path: crates/trusted-server-js/lib/package-lock.json + # The concurrency guard runs here because `cargo fmt` is a required check. + # Pin the yq binary by checksum, so a replaced release asset cannot run in + # CI with this job's token. + - name: Install yq + shell: bash + run: | + curl --fail --location --silent --show-error \ + https://github.com/mikefarah/yq/releases/download/v4.54.1/yq_linux_amd64 \ + --output "$RUNNER_TEMP/yq" + echo "8e34fc298390875de416e6a4afcb8cabeceb25d9aa8506c1a2f9353cf702ea5f $RUNNER_TEMP/yq" | sha256sum --check --strict + chmod +x "$RUNNER_TEMP/yq" + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + + - name: Test the CI concurrency policy guard + run: node --test scripts/check-ci-concurrency.test.mjs + + - name: Require per-PR cancellation, per-author job queues and timeouts + run: scripts/check-ci-concurrency.sh + - name: Run cargo fmt uses: actions-rust-lang/rustfmt@v1 @@ -136,6 +177,10 @@ jobs: format-typescript: runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-format-typescript-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: crates/trusted-server-js/lib @@ -167,6 +212,10 @@ jobs: format-docs: runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-format-docs-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: docs @@ -208,6 +257,10 @@ jobs: docs-proposal-scripts: name: docs-proposal scripts runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-docs-proposal-scripts-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 8500af3508..a7231cd842 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -10,6 +10,16 @@ on: types: [opened, synchronize, reopened] workflow_dispatch: +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + env: ORIGIN_PORT: 8888 ARTIFACTS_DIR: /tmp/integration-test-artifacts @@ -23,6 +33,9 @@ jobs: name: prepare integration artifacts runs-on: ubuntu-latest timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-prepare-artifacts-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -64,6 +77,9 @@ jobs: needs: prepare-artifacts runs-on: ubuntu-latest timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-integration-tests-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -119,6 +135,9 @@ jobs: needs: prepare-artifacts runs-on: ubuntu-latest timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-integration-tests-fastly-ec-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -159,6 +178,9 @@ jobs: # Playwright cannot install chromium on the ubuntu-latest 26.04 image. runs-on: ubuntu-24.04 timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-trace-runtime-tests-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -254,6 +276,9 @@ jobs: # once the pinned Playwright version supports the newer image. runs-on: ubuntu-24.04 timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-browser-tests-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pbs-example.yml b/.github/workflows/pbs-example.yml index dced0c7b63..0282e5a066 100644 --- a/.github/workflows/pbs-example.yml +++ b/.github/workflows/pbs-example.yml @@ -17,11 +17,24 @@ on: permissions: contents: read +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: local-checks: name: PBS Terraform and Compose checks runs-on: ubuntu-latest timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-local-checks-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max env: TF_IN_AUTOMATION: 'true' AWS_EC2_METADATA_DISABLED: 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60af0bb5d1..f2315fd21a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,24 @@ on: branches: [main] pull_request: +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: test-rust: name: cargo test runs-on: ubuntu-latest timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-test-rust-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -79,6 +92,9 @@ jobs: name: HTML EOF runtime (Fastly) runs-on: ubuntu-latest timeout-minutes: 25 + concurrency: + group: ${{ github.workflow }}-test-fastly-eof-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -127,6 +143,10 @@ jobs: test-axum: name: cargo test (axum native) runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-test-axum-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -208,6 +228,10 @@ jobs: test-cloudflare: name: cargo check (cloudflare native + wasm32-unknown-unknown) runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-test-cloudflare-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -242,6 +266,10 @@ jobs: test-spin: name: cargo check/build/test (spin native + wasm32-wasip1) runs-on: ubuntu-latest + timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-test-spin-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -288,6 +316,10 @@ jobs: test-parity: name: cargo test (cross-adapter parity) runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-test-parity-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -322,6 +354,10 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-test-cli-${{ matrix.os }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -394,6 +430,10 @@ jobs: name: ts dev lint domains (changed lines) if: github.event_name == 'pull_request' runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-lint-domains-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: # A PR checkout is GitHub's merge commit; depth 2 also fetches its # parents, so `HEAD^1` is the exact base commit the PR merges into. @@ -428,6 +468,10 @@ jobs: test-typescript: name: vitest runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-test-typescript-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: crates/trusted-server-js/lib diff --git a/.github/workflows/worker-auction-deadlines.yml b/.github/workflows/worker-auction-deadlines.yml index dda7e26e8d..3335736c4d 100644 --- a/.github/workflows/worker-auction-deadlines.yml +++ b/.github/workflows/worker-auction-deadlines.yml @@ -8,10 +8,23 @@ on: permissions: contents: read +# New pushes cancel older runs for the same pull request. Each job queues by +# workflow, job, and author, so one author runs one copy of each job at a time. +# GitHub orders jobs by when they start waiting, but does not guarantee FIFO. +# Each queue holds up to 100 pending jobs; later arrivals are canceled. +# Timeouts release queues after hung jobs. Bots and non-PR events use unique +# run IDs and do not queue together. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: worker-deadlines: runs-on: ubuntu-latest timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-worker-deadlines-${{ matrix.adapter }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max strategy: fail-fast: false matrix: diff --git a/.gitignore b/.gitignore index 770aa83ef8..41dc3aaaf4 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,6 @@ src/*.html # Documentation proposal workflow scratch space /.docs-proposal/ +# Wrangler config rendered by the Cloudflare integration-test harness from +# wrangler.ci.toml for CI or test-specific configuration overrides. +wrangler.integration.generated.toml diff --git a/AGENTS.md b/AGENTS.md index d90eda1f2d..7164165eb4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -377,6 +377,7 @@ Every PR must pass: 13. CI Node pin guard: `node --test scripts/check-ci-node-pin.test.mjs && node scripts/check-ci-node-pin.mjs` with the `.tool-versions` Node (every CI job that compiles Rust sets up the `.tool-versions` Node before its first cargo step) 14. Fastly absolute-deadline network fixtures under Viceroy (the harness builds the adapter test binary itself): `node crates/trusted-server-adapter-fastly/tests/deadline-origin.mjs "$(command -v viceroy)"` 15. Worker auction-deadline runtime fixtures (`worker-auction-deadlines.yml`): build and lint the runtime fixture, then run it under the local runtime. For Cloudflare, `cargo build --locked -p trusted-server-adapter-cloudflare --example deadline-runtime --target wasm32-unknown-unknown --features cloudflare` and `node scripts/test-worker-auction-deadlines.mjs cloudflare --runtime "$(command -v wrangler)"` (requires wrangler and the `Cargo.lock` wasm-bindgen CLI). For Spin, `cargo build --locked -p trusted-server-adapter-spin --example deadline-runtime --target wasm32-wasip1 --features spin` and `node scripts/test-worker-auction-deadlines.mjs spin --runtime "$(command -v spin)"`. CI lints each fixture with the same `cargo clippy` arguments plus `-- -D warnings` +16. CI concurrency policy guard: `node --test scripts/check-ci-concurrency.test.mjs && scripts/check-ci-concurrency.sh` (requires yq v4); every PR-triggered workflow must use `test.yml`'s per-PR cancellation block, and every non-reusable job must set a timeout and a per-author `queue: max` group. Matrix jobs must also include their matrix values in the group. CI also runs these HTML EOF checks. Run them locally only when changing EOF finalization, publisher or HTML-processor code: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5d0b9ea8..f418058af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Fastly abandons unfinished requests; Axum, Cloudflare, and Spin cancel owned work. - Explicit TS Console activation with `?ts_console=1` now sets a 30-minute diagnostics cookie instead of a browser-session cookie, independently of the mobile trace flag. Ordinary requests do not refresh it; end and explicitly enable diagnostics again to adopt the lifetime for an existing session. - Publisher documents with diagnostics active now use private, no-store responses and strip conditional and range request headers before fetching the origin. An unexpected origin `304` becomes a private `502` because it cannot supply an instrumented document, including when mobile tracing is disabled. +- Access telemetry v2 now uses `auction_collected_ms` for collection return, sets `ts_version` to the TS build identifier matching `x-ts-version`, and adds `delivery_result` for local complete/partial/error transport handoff. It does not establish client receipt. Coordinate any deployed preview v2 schema before updating the producer. +- Access ingestion now dispatches without waiting for Tinybird's reply. Start failures are reported; later transport errors, non-2xx responses and remote ingestion acceptance are unobserved, with no retry. +- `/auction`, page-bids and HTML collectors share measured collection waits and outcome mapping. Real mediator-only work can have collection/wait/commit timing without a provider dispatch offset; routing and admission still decide whether any request is sent. +- Prebid diagnostics listener, targeting and correlation code now lives in the optional diagnostics bundle. Badge evidence passes clicks through, its small control sits outside observed creative bounds, and selecting the latest request no longer opens history. + +- Auction telemetry failure batches for `/auction` and page-bids now include provider launch-failure rows alongside the summary, so a failed dispatch can emit more than one row. `/auction` summary `elapsed_ms` now uses the orchestrator's elapsed time. +- GPT diagnostics JSON exports now include bounded server winner and Prebid candidate/win bidder codes and bucketed `hb_pb` values. These observations do not establish which creative GAM ultimately served; raw CPM and creative markup remain excluded. +- The Axum dev server now runs its own serve loop to apply terminal request-timing handling. +- The default `tinybird.access_dataset` is now `access_logs_v2`. Since this field is serialized, an upgrade from TOML that omits it may produce a one-time config-envelope diff; review and push once to settle it. Explicit legacy dataset values must be changed before enabling access telemetry. +- Consent decoding for identity providers is stricter: a raw consent string that cannot be decoded blocks a provider whose policy is strict, and the GPP sharing, targeted-advertising and GPC fields of the US sections now block it too. A header-only GPP string with no applicable section does not block where TCF does not apply. +- The Lockr proxy now logs only the path of the upstream URL, never its query string. +- Identity KV is written at most once per EC-bearing request. Only a full page navigation from a returning visitor (the writer request) writes KV, once, after the response is sent: browser EIDs, pull partner results and the cluster size all go in that one write, with a generation check and no retry of the whole write. Every other route reads KV and stages but writes nothing. A consent-withdrawal tombstone and the removal-only retry of a failed write are the only exceptions. Mid-branch builds drop browser ingest until the whole change ships. +- `POST /auction`, `GET /_ts/page-bids` and `GET /_ts/api/v1/identify` no longer write KV, and `/_ts/api/v1/identify` omits expired IDs. The `/identify` response shape is unchanged: one UID per source. +- Pull partners now run from a visitor's second page load, from the writer request after send, and back off on errors. They are paced by `[ec.identity.providers.pull-]` (`refresh_after_sec`, `max_calls_per_minute`), so the old default of 10 calls per hour becomes an effective 60 per hour. That 60 per hour is a ceiling, not a rate: `refresh_after_sec` and error backoff gate each partner first. Runtime startup now builds each pull partner's descriptor: a partner whose allowed host fails the descriptor host check stops startup. See [Identity Providers](docs/guide/configuration.md#identity-providers-ec-identity). +- **Breaking:** `ec.pull_sync_concurrency`, and `pull_sync_ttl_sec` and `pull_sync_rate_limit` on `[[ec.partners]]`, are removed, and validation now rejects them with an error that names the replacement. Use `[ec.identity].max_concurrency` (the example config's value moves from 3 to the default 4), `refresh_after_sec`, and `max_calls_per_minute = ceil(N / 60)` for `pull_sync_rate_limit = N`, the last two in `[ec.identity.providers.]`. +- Identity providers that resolve must now declare the TCF purposes their GVL entry lists (`ConsentPolicy::with_tcf_purposes`) and their GVL ID (`with_tcf_vendor`), or opt out of vendor consent with `without_tcf_vendor`; `ConsentPolicy::strict()` alone now requires only Purpose 1, and `build()` rejects an undeclared resolver. +- A new visitor's first navigation that runs an auction now runs hold-mode identity providers that do not read the EC ID, and creates its row with their answers in the one create write, so a visitor who leaves after one page can still get a provider ID. It waits up to `hold_budget_ms`; without such a provider the create runs before routing as before. +- A same-origin `/_ts/page-bids` call from a real browser now writes browser ingest and after-send provider results, at most once per `ec.identity.spa_writer_interval_sec` (default 300, `0` turns it off), so single-page apps do not wait for a full navigation. +- A removal whose KV writes all fail now waits in a `pending:` KV key (one-day TTL) for the next writer navigation to merge, counted in `removal_deferred`; `removal_lost` counts only a removal that could not reach that key either. Positive IDs a capture sees on a request that is not the writer wait there too, so a capture's full entry set reaches KV. Every returning writer navigation reads that key once, a second KV read. +- A global revoke (`revoke_scope = "global"`) now also sets an `HttpOnly` `ts-ec-revoked` cookie with a fingerprint of the visitor's consent, and no EC is minted until that consent changes, so a CMP that still grants does not undo the revoke on the next page. +- `max_calls_per_ip_per_minute` (default 10) on each `[ec.identity.providers.]` caps calls to that provider from one client address across visitors, next to the per-visitor cap. +- `[ec.identity] max_backend_requests` (default 32, Fastly's per-execution limit) bounds the origin, auction and identity provider calls of one request together. A provider call past it is skipped and is not counted as a failure, so it never opens the provider's circuit breaker. Hold-mode calls also cap their connect timeout at their call timeout. +- A provider opt-out clears on a TCF re-grant only when the opt-out was recorded under a TCF denial that is now granted; a later CMP re-save that was already granting no longer clears it. A clear records the opt-out it lifted rather than its own time, so it cannot lift an opt-out written after its request read the row. +- `Resolves::with_refresh_grace` lets an identity provider without private state keep an expired ID it wrote for a source it owns as a refresh anchor until the expiry plus the grace. The ID is never served meanwhile. +- With `[ec.identity] mirror_enabled = true` (off by default), `/auction` responses carry `ext.ts_eids_s` and `/_ts/page-bids` responses carry `ts_eids_s`: the payload (`value` with `max_age`, `clear`, or absent) for the client-only `__Host-ts-eids-s` mirror cookie, at most 2,048 bytes and one day. The shim now writes only `[[ec.partners]]` sources into `ts-eids`, merging each auction's entries per source instead of replacing the cookie, and no longer clears it when a request carries no EIDs (a TCF block still clears it). +- The `ts-ec-pull-complete` cookie is removed: Trusted Server no longer sets or reads it, and a copy a browser holds expires within one hour. +- **Breaking:** The KV identity row moves to schema version 2. A release before this one cannot read a version 2 row, so rolling back past this release is unsupported once it has written one. See [Rolling back](docs/guide/configuration.md#rolling-back). This release should merge after the EID-fidelity changes, whose behaviour its golden tests pin. +- The `EID sync measurement: ...` log line is no longer logged for returning visitors; only a new EC still logs it. Each request now logs one `identity request measurement: ...` line after the response is sent, with the inbound `Cookie` header size, KV reads and writes by route kind, the flush outcome, lost removals and the after-send withdrawal run. It holds no identifiers. Operator dashboards that read the old line should read the new one. +- **Breaking:** Prebid Server no longer receives any Trusted Server identity cookie (`ts-ec`, `ts-eids`, and the `__Host-ts-eids-s` mirror that this release's shim writes); today it receives `ts-ec` and `ts-eids`. The publisher origin still receives `ts-ec` and `ts-eids` but never the mirror. `[ec] strip_eids_cookie_from_origin = true` strips `ts-eids` from origin requests too. - **Breaking:** `x-ts-version` now reports the deployed git version — the release tag, else the branch, else the first 6 characters of the commit — compiled in from the build-time `TRUSTED_SERVER__GIT_VERSION` (set by the deploy pipeline) or local git, and is sent by every adapter, including on the Fastly `GET /health` probe. The Fastly service version it previously carried is now `x-ts-fastly-version`; update dashboards, monitors, and scripts that read `x-ts-version` as the Fastly version number. Builds with neither the override nor git omit the header. - **Breaking:** `ts audit ad-templates generate` now previews the managed-field diff on stderr and asks before writing a changed config. Runs whose stdin or stderr is not a terminal fail without writing unless `--yes` is passed, so add `--yes` to scripts and CI jobs that apply generated slots. A declined prompt exits non-zero, `--yes` cannot be combined with `--dry-run`, and a candidate whose managed fields are unchanged is no longer rewritten. - **Breaking:** `ts prebid bundle` is now `ts prebid client`, alongside the new `ts prebid server` namespace. Update scripts and runbooks to use `ts prebid client` with the same arguments. The old `bundle` spelling is no longer accepted and has no compatibility alias. @@ -52,6 +81,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Lockr and Permutive SDK recognition now uses parsed vendor hostnames and SDK pathnames instead of searching the whole URL (#1199). The browser Lockr guard no longer replaces unrelated scripts that mention a Lockr host in their path or query, use a lookalike host, or serve a non-SDK resource from a Lockr host. Query strings and fragments no longer prevent SDK recognition in the server matchers, the browser Permutive guard, or the browser Lockr guard on `identity.loc.kr`. Matching URLs use the existing configured-SDK routes; their original query and fragment are not forwarded upstream. Server matching trims leading and trailing C0 controls and spaces, removes tab, CR, and LF, and accepts backslashes as authority separators. It no longer recognizes attribute values that contain a NUL or that write slashes as character references such as `/`, so markup like that can load the SDK from the vendor host. - `rewrite.exclude_domains` matching is now case-insensitive and ignores surrounding whitespace. Entries written with uppercase letters previously never matched and now take effect, so those hosts stop being proxied and click-wrapped, and `/first-party/sign` now rejects them with `502`; it used to sign them, or return `403` when the host was also outside `proxy.allowed_domains`. Empty and bare `"*"` `exclude_domains` entries, which never matched a host, are dropped at load with a warning, and other entries that can never match a host are logged with a warning. Absolute `http(s)` creative URLs that cannot be parsed (for example, a host containing a space) are now left untouched; previously the trimmed value was written back re-quoted (with `https:` prepended to `//` input, so `src=" //example.com invalid/x "` became `https://example.com invalid/x`), an unparseable quoted URL in `