Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3ff6054
Add the provider documentation set and finish the decomposition
jwrosewell Aug 19, 2026
1f6eea4
Correct the documentation set against the code
jwrosewell Aug 27, 2026
aba3399
Remove the test fixture and import the evidence rewrite left behind
jwrosewell Aug 31, 2026
72542e3
Put back the check on what a provider is handed with no client IP
jwrosewell Aug 31, 2026
e28ffc8
Run the core library tests natively in CI as well as on WebAssembly
jwrosewell Aug 31, 2026
5494e0f
Documentation and house-style cleanup for the response-hook and docs PR
jwrosewell Aug 31, 2026
bcc9a5b
Drop the retired default_country from the configuration guide
jwrosewell Sep 1, 2026
158c129
Correct the configuration guide's permissions file location
jwrosewell Sep 2, 2026
6534bbd
Merge the permission signal work from split/4
jwrosewell Sep 14, 2026
13c27d7
Merge the follow-up commits from split/4
jwrosewell Sep 14, 2026
d484805
Merge main into the response hook docs through split/4
jwrosewell Sep 14, 2026
3d04183
Say the host-signal provider also hashes the client IP
jwrosewell Sep 14, 2026
8b7cd94
Stop naming the retired default_country in the geo guide
jwrosewell Sep 14, 2026
5406acf
Merge the Edge Cookie review follow-ups through split/4
jwrosewell Sep 14, 2026
65f7005
Merge the permissions inspector fix from split/4
jwrosewell Sep 14, 2026
672cc60
Merge the permissions sample rename from split/4, and follow it in the
jwrosewell Sep 14, 2026
6004c99
Merge the four provider renames from split/4, and follow them in the …
jwrosewell Sep 15, 2026
d11895b
Join four clauses in the Edge Cookie guides with words rather than marks
jwrosewell Sep 15, 2026
f97a328
Merge main into the provider documentation set
jwrosewell Sep 25, 2026
1feaab6
Merge the test state fix from split/4
jwrosewell Sep 25, 2026
02782ab
Merge the fixture jurisdiction fix from split/4
jwrosewell Sep 25, 2026
33503ed
Merge the withdrawal test fix from split/4
jwrosewell Sep 25, 2026
3c77e44
Merge split/4-client-resolve at ff56a5fcd into split/5-response-hook-…
jwrosewell Oct 6, 2026
caccf8d
The documentation set says module where it said provider
jwrosewell Oct 6, 2026
ecb5fb7
Merge split/4-client-resolve at 17fd681eb into split/5-response-hook-…
jwrosewell Oct 6, 2026
30fbc6d
Merge split/4-client-resolve at e3aeeba08 into split/5-response-hook-…
jwrosewell Oct 6, 2026
833089e
Merge split/4-client-resolve at 9a3772871 into split/5-response-hook-…
jwrosewell Oct 7, 2026
93af3ea
The configuration guide states the module name rule
jwrosewell Oct 7, 2026
327211c
Merge split/4-client-resolve at 6b9f52b40 into split/5-response-hook-…
jwrosewell Oct 7, 2026
f220687
The Edge Cookie guides describe the permission model as it is
jwrosewell Oct 7, 2026
1f31ba5
Merge split/4-client-resolve at 59660c468 into split/5-response-hook-…
jwrosewell Oct 7, 2026
3c7ef00
Merge split/4-client-resolve at e7623de78 into split/5-response-hook-…
jwrosewell Oct 8, 2026
61b1399
Merge split/4-client-resolve into split/5-response-hook-docs
Jamesr51d Oct 13, 2026
b5a4fcc
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 15, 2026
c8cbf48
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 16, 2026
d989f58
The test loader leaves out the build's git version variable
jwrosewell Oct 16, 2026
60b3f79
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 16, 2026
f278c09
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 16, 2026
058c739
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 18, 2026
8e0761d
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 18, 2026
bae9bc8
The Edge Cookie guide says the identifier is kept in the ts-ec cookie
jwrosewell Oct 18, 2026
45ee0db
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 18, 2026
55f3fab
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 19, 2026
e4a8450
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 19, 2026
c8f4012
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 19, 2026
643de85
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 19, 2026
48a0efa
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
17ed836
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
e231669
The configuration guide says the policy file is selected when deployed
jwrosewell Oct 28, 2026
5938788
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
4f8fa2e
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
289b0b5
The configuration guide says no adapter build carries a policy
jwrosewell Oct 28, 2026
7805e00
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
ca9dcbd
Merge split/4-client-resolve into split/5-response-hook-docs
jwrosewell Oct 28, 2026
40cf6a3
The policy guard steps run on Linux alone
jwrosewell Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ jobs:
run: BID_DELAY=3 ./scripts/template-cache-local-test.sh purge

test-axum:
name: cargo test (axum native)
runs-on: ubuntu-latest
name: cargo test (axum native, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -110,16 +114,29 @@ jobs:
run: |
cargo test --package trusted-server-openrtb-codegen --target "$(rustc -vV | sed -n 's/host: //p')"

# The core library's unit tests otherwise run only on the WebAssembly
# targets, which build with panic=abort, so their harness stops at the
# first failing test and reports every later one as never run. A run that
# looks like a single failure can hide many more. This native run reports
# them all at once, which is what makes a red build readable.
- name: Run host-target core library tests
run: |
cargo test --package trusted-server-core --target "$(rustc -vV | sed -n 's/host: //p')"

- name: Verify Fastly WASM release build
run: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1

# The guard's two steps are written for bash and run on Linux alone,
# because the WASM they check is the same on every system.
- name: Check that no permission policy is compiled into the Fastly WASM
if: runner.os == 'Linux'
run: ./scripts/check-no-compiled-policy.sh target/wasm32-wasip1/release/trusted-server-adapter-fastly.wasm

# The sample carries both markers the guard looks for, so a guard that
# stops finding them passes the sample and fails the job here. Exit 1 is
# the refusal; exit 2 means the guard could not run.
- name: Check that the policy guard refuses a file that carries a policy
if: runner.os == 'Linux'
run: |
status=0
./scripts/check-no-compiled-policy.sh config/permissions/sample.yaml || status=$?
Expand All @@ -129,8 +146,12 @@ jobs:
fi

test-cloudflare:
name: cargo check (cloudflare native + wasm32-unknown-unknown)
runs-on: ubuntu-latest
name: cargo check (cloudflare native + wasm32-unknown-unknown, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
/spin
/spin.sig

# logs
*.log

# EdgeZero local KV store (created by edgezero-adapter-axum framework)
.edgezero/
/dist/prebid/
Expand Down
5 changes: 3 additions & 2 deletions crates/geo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ implementing the `PlatformGeo` trait from `trusted-server-core`:

Whatever the source, a module returns the same `GeoInfo` coding. The country
is an ISO 3166-1 alpha-2 code (`US`) and the region is the ISO 3166-2 subdivision
code with no country prefix (`CA`), so the Fastly and other modules feed the
same downstream rules without translation.
code with no country prefix (`CA`). The permission model keys its country and
region rules on these codes, matched case-insensitively, so the Fastly and
other modules feed the same rules without translation.

The platform-neutral `PlatformGeo` trait and `DisabledGeo` both live in
`trusted-server-core`. With no `[geo] module`, or `module = "platform"`, the
Expand Down
20 changes: 19 additions & 1 deletion crates/trusted-server-core/src/ec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1653,6 +1653,7 @@ pub(crate) mod tests {
#[derive(Debug, Default)]
struct EvidenceCapturingModule {
seen: std::sync::Mutex<Option<(String, String)>>,
seen_client_ip: std::sync::Mutex<Option<String>>,
}

impl EdgeCookieModule for EvidenceCapturingModule {
Expand All @@ -1672,6 +1673,11 @@ pub(crate) mod tests {
let query_id = request_info.query_param("id").unwrap_or_default();
let cookie = request_info.header("cookie").unwrap_or_default().to_owned();
*self.seen.lock().expect("should lock seen evidence") = Some((query_id, cookie));
*self
.seen_client_ip
.lock()
.expect("should lock the seen client IP") =
Some(request_info.client_ip().to_owned());
Ok(GeneratedEdgeCookie {
id: Some("evidence-ec".to_owned()),
response_headers: Vec::new(),
Expand Down Expand Up @@ -1847,7 +1853,10 @@ pub(crate) mod tests {
// The requirement for a client IP belongs to the module that uses
// one, not to core. A module deriving identity from the request
// query and cookies runs on a host that cannot determine a client IP,
// and receives the documented unavailable value, the empty string.
// and still creates an identifier. This also pins what the module is
// handed in that case, which is the documented unavailable value rather
// than something else, because a module cannot decide how to behave
// without knowing what absence looks like.
let module = Arc::new(EvidenceCapturingModule::default());
let mut settings = create_test_settings();
settings.ec.module = Some(EcModuleSelection::from("evidence"));
Expand All @@ -1874,6 +1883,15 @@ pub(crate) mod tests {
Some("t0ev~evidence-ec"),
"the identifier should be committed with no client IP available"
);
assert_eq!(
module
.seen_client_ip
.lock()
.expect("should lock the seen client IP")
.clone(),
Some(String::new()),
"a host that cannot determine a client IP should hand the module the documented unavailable value, which is the empty string"
);
}

#[test]
Expand Down
44 changes: 26 additions & 18 deletions crates/trusted-server-core/src/evidence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,35 @@
//! per-request is stored on the module, so the same module serves every
//! request.
//!
//! These traits are those views. Request-scoped data outlives the live request
//! only when snapshotted, so an implementation owns its data where needed.
//! [`BorrowedRequestInfo`] is the borrowed view core builds on the request
//! path, and [`OwnedRequestInfo`] is the built-in owned snapshot.
//! These traits are the service interfaces. Concrete implementations live with
//! the host or vendor that supplies them (for example `FastlyHostSignals` in
//! `trusted-server-device-fastly`). An injected service outlives the borrow of
//! the live request, so an implementation owns its data ([`OwnedRequestInfo`] is
//! the built-in owned snapshot, while [`BorrowedRequestInfo`] is the borrowed
//! view core builds on the request path and must not outlive the request).
//!
//! [`RequestInfo`] describes what a request carries, not what code in this
//! repository happens to read today, so it carries the whole of it whether or
//! not anything here reads it yet. What a module may see is not the control.
//! What a module may do with what it sees is the control, and that is the
//! permission model.

use http::HeaderMap;

/// Host-computed client signals that are not carried in request headers.
///
/// A host that can compute them supplies an implementation (Fastly exposes the
/// TLS JA4 and HTTP/2 signals). A module that needs them takes
/// `Arc<dyn HostSignals>` in its constructor, and on a host that supplies none
/// the module cannot be built and the request stops.
pub trait HostSignals: Send + Sync + core::fmt::Debug {
/// The full JA4 TLS signal, or `None` when unavailable.
fn ja4(&self) -> Option<&str>;

/// The raw HTTP/2 SETTINGS signal, or `None` when unavailable.
fn h2(&self) -> Option<&str>;
}

/// Read-only access to the current request's basic information.
///
/// The request data any host can supply: the normalized client IP, the
Expand Down Expand Up @@ -253,20 +275,6 @@ impl RequestInfo for BorrowedRequestInfo<'_> {
}
}

/// Host-computed client signals that are not carried in request headers.
///
/// A host that can compute them supplies an implementation (Fastly exposes the
/// TLS JA4 and HTTP/2 signals). A module that needs them takes
/// `Arc<dyn HostSignals>` in its constructor, and on a host that supplies none the
/// module cannot be built and the request stops.
pub trait HostSignals: Send + Sync + core::fmt::Debug {
/// The full JA4 TLS signal, or `None` when unavailable.
fn ja4(&self) -> Option<&str>;

/// The raw HTTP/2 SETTINGS signal, or `None` when unavailable.
fn h2(&self) -> Option<&str>;
}

#[cfg(test)]
mod cookie_tests {
use http::HeaderMap;
Expand Down
15 changes: 15 additions & 0 deletions crates/trusted-server-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8003,6 +8003,21 @@ source_domain = "partner.example.com"
);
}

#[test]
fn from_toml_and_env_loads_while_the_git_version_variable_is_set() {
temp_env::with_var(
format!(
"{}{}GIT_VERSION",
ENVIRONMENT_VARIABLE_PREFIX, ENVIRONMENT_VARIABLE_SEPARATOR
),
Some("v1.0.0"),
|| {
Settings::from_toml_and_env(&crate_test_settings_str())
.expect("should load settings while the build's git version variable is set");
},
);
}

#[test]
fn test_override_env() {
let toml_str = crate_test_settings_str();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
// Client and server share one fixed, known word. When the resolved marker is
// absent, this posts that word to the resolve endpoint. With the `client_fixed`
// module selected, the server verifies the word and, on a match, persists the
// identity-graph row and sets the word as an HttpOnly Edge Cookie on the
// identity-graph row and sets the coded form of the word (cfix~an-ec) as an
// HttpOnly Edge Cookie on the
// response, together with a non-HttpOnly marker cookie. The Edge Cookie itself
// is HttpOnly, so this script can never see it; the marker is what tells it a
// resolve already succeeded, so it does not post again on every page view.
Expand Down
6 changes: 3 additions & 3 deletions docs/guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,11 @@ mappings are accepted even though the second A appears after B in the input.

Resolve endpoint for client-side Edge Cookie modules. The page posts a value that the module verifies and creates the Edge Cookie value. Used only when a client-side module is selected (for example the `client_fixed` demo). Server-side modules such as HMAC do not use it.

**Auth:** None. This is a first-party POST from the page. The module is responsible for verifying the posted value before trusting it.
**Auth:** None, but the request must carry an `Origin` on the publisher's own domain (a foreign or missing `Origin` answers `403`). This is a first-party POST from the page. The module is responsible for verifying the posted value before trusting it.

**Request Body:** the module's value, opaque to the core. For the `client_fixed` demo this is the fixed known word sent as `text/plain`.

**Behavior:** gated by the [permission model](/guide/permission-model) exactly like organic generation. On success the EC cookie is set on this response (`HttpOnly`, `Secure`, `SameSite=Lax`) and the status is `200`. When the gate is closed, no client-side module is configured, or the module produces no identifier, the response is `204` with no cookie. An oversized body is rejected with `413`.
**Behavior:** gated by the [permission model](/guide/permission-model) exactly like organic generation. On success the identifier is written to the identity graph first, then the EC cookie is set on this response (`HttpOnly`, `Secure`, `SameSite=Lax`) together with the `ts-ecr` marker cookie the page script can read, and the status is `200`. When the gate is closed, no client-side module is configured, no identity graph is available, or the module produces no identifier, the response is `204` with no cookie. Rejections: `403` for a missing or foreign `Origin`, `415` for a content type other than `text/plain` or `application/json`, `413` for an oversized body, `400` when the created identifier is outside the identifier bounds, `409` when the request already carries a different identity, and `503` when the identity-graph write fails. Every response the handler builds carries `Cache-Control: no-store`.

---

Expand Down Expand Up @@ -913,7 +913,7 @@ curl -u 'admin:<resolved-admin-password>' \

### GET /\_ts/admin/eids

Parses the request's `ts-eids` and `sharedId` cookies and previews which configured partner IDs cookie ingestion would match or drop. It performs request inspection only: it does not read or write KV and is available on every adapter. The preview lists candidate matches from the cookies alone; it does not apply the TCF Purpose 1 + 4 consent gate on identity-graph writes, does not see `/auction` request-body EIDs, and does not account for partner UIDs already stored in KV.
Parses the request's `ts-eids` and `sharedId` cookies and previews which configured partner IDs cookie ingestion would match or drop. It performs request inspection only: it does not read or write KV and is available on every adapter. The preview lists candidate matches from the cookies alone; it does not apply the permission gate on identity-graph writes, does not see `/auction` request-body EIDs, and does not account for partner UIDs already stored in KV.

After successful authentication this endpoint always returns `200 OK`; missing or malformed cookies are represented by `cookie_present`, `sharedid_present`, and `parse_error`. The `ingest.matched` and `ingest.unmatched` arrays show the ingestion preview. Each unmatched entry contains its `source` and either a `no_partner` reason when no configured partner recognizes it or `no_valid_uid` when the partner exists but every supplied UID is empty or exceeds the storage limit.

Expand Down
Loading
Loading