Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
b740265
Add a pluggable Edge Cookie provider seam with the built-in HMAC prov…
jwrosewell Aug 18, 2026
d2ad2a9
Accept the provider-code envelope on the partner-facing identifier paths
jwrosewell Aug 27, 2026
b2623cb
Rename the legacy passphrase migration so CodeQL stops tainting Settings
jwrosewell Aug 27, 2026
918f96c
Stop serving without identity when a selected provider is unavailable
jwrosewell Aug 28, 2026
61b7cf1
Hold the deprecated EC passphrase to the same rules as the new block
jwrosewell Aug 28, 2026
5b01f98
Reject unknown keys in the built-in HMAC provider block
jwrosewell Aug 28, 2026
7424932
Stop rather than run stateless when the hmac block is missing
jwrosewell Aug 28, 2026
d6dc848
Restore the missing line continuation in the mint rejection message
jwrosewell Aug 28, 2026
45ec829
Give EdgeCookieProvider its own doc comment back
jwrosewell Aug 28, 2026
c4c584c
Delete the unused ec::get_ec_id helper
jwrosewell Aug 28, 2026
5203c2d
Correct the provider module docs about when evidence arrives
jwrosewell Aug 28, 2026
e44ff38
Replace the scattered EC provider key strings with a typed selector
jwrosewell Aug 28, 2026
afdb7be
Reserve core's own response surface against provider effects
jwrosewell Aug 29, 2026
e90b471
Dispatch partner-path identifier checks by provider code
jwrosewell Aug 29, 2026
3b74d74
Let each provider decide whether it needs the client IP
jwrosewell Aug 29, 2026
29a3a7f
State a real retirement condition for the legacy bare-identifier reader
jwrosewell Aug 29, 2026
cb62786
Key identity-graph reads and write-backs by the canonical form
jwrosewell Aug 29, 2026
fe23dcc
Egress only an Edge Cookie identifier the provider recognizes
jwrosewell Aug 29, 2026
e3ba579
Record the cluster-count gap the identifier envelope opens
jwrosewell Aug 29, 2026
292df1f
Consume the refused Report in the testlight egress tests
jwrosewell Aug 29, 2026
a3a5d57
Drop the request-evidence accessors that have no caller
jwrosewell Aug 29, 2026
b70ddc0
Collapse the EC provider selector to statelessness and a named provider
jwrosewell Aug 29, 2026
5da52c8
Accumulate provider response headers instead of replacing the origin's
jwrosewell Aug 29, 2026
67c3b0b
State what a provider switch really does to existing identities
jwrosewell Aug 29, 2026
1f62e1a
Name the design documents rather than their paths in doc comments
jwrosewell Aug 30, 2026
c7464bc
Restore the line continuations missed in the neighbouring files
jwrosewell Aug 30, 2026
ef81e88
Correct the two provider doc comments the earlier pass missed
jwrosewell Aug 30, 2026
acb416f
Stop a provider code from panicking when a vendor builds one at run time
jwrosewell Aug 30, 2026
8eb0a9b
Refuse two Edge Cookie providers claiming the same name
jwrosewell Aug 30, 2026
f2b1825
Build the internal header list from the Edge Cookie response headers
jwrosewell Aug 30, 2026
52e2692
Resolve the Edge Cookie provider once per request instead of twice
jwrosewell Aug 30, 2026
1c7df60
Load Spin settings from the config store instead of a baked template
jwrosewell Aug 30, 2026
3234092
Stop exposing an inbound Edge Cookie identifier nothing has vouched for
jwrosewell Aug 30, 2026
1df8bf7
Keep the whole request-evidence interface
jwrosewell Aug 30, 2026
8a29373
Documentation and house-style cleanup for the Edge Cookie provider PR
jwrosewell Aug 31, 2026
147a37b
Name the caching reason accurately in the reserved-header rejection
jwrosewell Aug 31, 2026
7dda9dd
Collapse Edge Cookie provider injection to a single seam
jwrosewell Aug 31, 2026
c16c99d
Remove the orphaned no-client-IP test fixture
jwrosewell Sep 1, 2026
99b1acd
Add device and geo provider selection with the host-signal Edge Cooki…
jwrosewell Aug 19, 2026
20fee6a
Omit an unset provider selector from the serialized config
jwrosewell Aug 30, 2026
74047c1
Read the Cloudflare visitor region so US state opt-outs are honored
jwrosewell Aug 30, 2026
cf8d098
Reunite the request-info builders with their own documentation
jwrosewell Aug 31, 2026
de98934
Documentation and house-style cleanup for the device and geo provider PR
jwrosewell Aug 31, 2026
d3b4b06
Drop the unused HeaderMap import from the device tests
jwrosewell Sep 1, 2026
dcdb493
Give each vendor crate a visible maintainers declaration
jwrosewell Sep 2, 2026
8bb5987
Log the jurisdiction class rather than the value when EC creation is …
jwrosewell Sep 3, 2026
ebf0117
Merge branch 'main' into split/1-ec-provider
aram356 Sep 4, 2026
af2e2f5
Merge main into the Edge Cookie provider seam
jwrosewell Sep 14, 2026
cb28ad3
Build the Axum test state from the compiled auction plan
jwrosewell Sep 14, 2026
0832f00
Merge main into device and geo selection through split/1
jwrosewell Sep 14, 2026
a75571d
Resolve the host-signals passphrase from the secret store
jwrosewell Sep 14, 2026
0980b73
Describe rejected provider effects accurately
jwrosewell Sep 14, 2026
0e7f7eb
Key the remaining identity-graph paths canonically
jwrosewell Sep 14, 2026
fb1bc28
Correct comment claims and test a stale preload read
jwrosewell Sep 14, 2026
cb6f717
Keep provider headers only from candidates that generation commits
jwrosewell Sep 14, 2026
1b88e42
Correct the admin lookup forms and the bare reader row note
jwrosewell Sep 14, 2026
37eadae
Merge the Edge Cookie review follow-ups from split/1
jwrosewell Sep 14, 2026
6e61e0b
Name the host-signal Edge Cookie provider host_signals
jwrosewell Sep 15, 2026
14a3ffb
Give each identity provider its own table under [ec]
jwrosewell Sep 15, 2026
558bde5
Merge the per-provider [ec] tables from split/1
jwrosewell Sep 15, 2026
4d40e5c
Merge main into the Edge Cookie provider seam
jwrosewell Sep 25, 2026
5e32cde
Merge main into device and geo provider selection
jwrosewell Sep 25, 2026
5b83034
Give the test state the services field main added
jwrosewell Sep 25, 2026
c6bb840
Merge the test state fix from split/1
jwrosewell Sep 25, 2026
53f16ab
Merge upstream main 7a0ecb4 into split/1-ec-provider
jwrosewell Oct 6, 2026
797c919
Give orphan recovery the request's headers, path and query
jwrosewell Oct 6, 2026
fbe302a
Delete the cookie reading test that only read its own double
jwrosewell Oct 6, 2026
977456e
Test that push validation keeps a labeled block's other errors
jwrosewell Oct 6, 2026
4da2a39
Bring the Edge Cookie seam's comments, docs and tests into line
jwrosewell Oct 6, 2026
1e25353
The Edge Cookie seam says module where it said provider
jwrosewell Oct 6, 2026
80a842e
Merge split/1-ec-provider at 1e25353b8 into split/2-device-geo
jwrosewell Oct 6, 2026
2c34ed1
Stop refusing the host-signals spelling by name
jwrosewell Oct 6, 2026
badb58b
Resolve a cross-named built-in Edge Cookie block's passphrase once
jwrosewell Oct 6, 2026
13cfa9b
The device and geo seams say module where they said provider
jwrosewell Oct 6, 2026
6b2e303
A module is named by its crate folder
jwrosewell Oct 7, 2026
6fc9cdf
Merge split/1-ec-provider at 6b2e303f1 into split/2-device-geo
jwrosewell Oct 7, 2026
ef4c013
Comments say what the code does, with the history taken out
jwrosewell Oct 7, 2026
35ccc36
Merge split/1-ec-provider at ef4c01347 into split/2-device-geo
jwrosewell Oct 7, 2026
cec6052
Comments say what the code does, with the history and plans taken out
jwrosewell Oct 7, 2026
5d808f2
Say what normalize_id_for_kv decides, and pin it with a fixture
jwrosewell Oct 7, 2026
09514b1
Merge split/1-ec-provider at 5d808f233 into split/2-device-geo
jwrosewell Oct 7, 2026
6f52771
The identity graph's comments describe device records, not person rec…
jwrosewell Oct 7, 2026
fb1d369
Merge split/1-ec-provider at 6f527710a into split/2-device-geo
jwrosewell Oct 7, 2026
d33507b
Merge upstream main 182fdf4 into split/1-ec-provider
jwrosewell Oct 8, 2026
47551a6
Merge split/1-ec-provider at d33507b32 into split/2-device-geo
jwrosewell Oct 8, 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
8 changes: 4 additions & 4 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ test_details = "test --target aarch64-apple-darwin"
# native crate needs no change here. Axum (native), Cloudflare
# (wasm32-unknown-unknown), Spin, the CLI (native), and integration-tests
# (native) are simply not listed.
build-fastly = "build -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
check-fastly = "check -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
clippy-fastly = "clippy -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-js -p trusted-server-openrtb --all-targets --all-features --target wasm32-wasip1 -- -D warnings"
test-fastly = "test -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
build-fastly = "build -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
check-fastly = "check -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
clippy-fastly = "clippy -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-js -p trusted-server-openrtb --all-targets --all-features --target wasm32-wasip1 -- -D warnings"
test-fastly = "test -p trusted-server-core -p trusted-server-adapter-fastly -p trusted-server-device-fastly -p trusted-server-geo-fastly -p trusted-server-js -p trusted-server-openrtb --target wasm32-wasip1"
# Feature-on counterpart of `test-fastly`. `test-fastly` does NOT pass
# --all-features, so without this the reusable-sandbox code is linted by
# `clippy-fastly` but its tests never execute.
Expand Down
19 changes: 19 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[workspace]
resolver = "2"
members = [
"crates/device/fastly",
"crates/geo/fastly",
"crates/trusted-server-adapter-axum",
"crates/trusted-server-adapter-cloudflare",
"crates/trusted-server-adapter-fastly",
Expand Down Expand Up @@ -120,6 +122,8 @@ toml_edit = "0.23.10"
tower = "0.4"
tracing = "0.1"
trusted-server-core = { path = "crates/trusted-server-core" }
trusted-server-device-fastly = { path = "crates/device/fastly" }
trusted-server-geo-fastly = { path = "crates/geo/fastly" }
trusted-server-js = { path = "crates/trusted-server-js" }
trusted-server-openrtb = { path = "crates/trusted-server-openrtb" }
url = "2.5.8"
Expand Down
10 changes: 10 additions & 0 deletions crates/device/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Device modules

Device-detection module crates live here, one per vendor. The Fastly module
(`trusted-server-device-fastly`) classifies a request with the host's TLS and
HTTP/2 signals. Future vendor modules (for example
`crates/device/<vendor>`) slot in alongside it.

The built-in default module (User-Agent only) ships in `trusted-server-core`
(`ec::device`). Adapters select and inject the vendor module via
`build_device_module`.
25 changes: 25 additions & 0 deletions crates/device/fastly/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "trusted-server-device-fastly"
description = "Fastly host device module exposing opt-in TLS and HTTP/2 signals."
authors = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
publish = { workspace = true }
version = { workspace = true }

[lib]
doctest = false

[lints]
workspace = true

[dependencies]
trusted-server-core = { workspace = true }
fastly = { workspace = true }

# The visible owner of this vendor crate, the way Prebid.js requires a named
# maintainer of every adapter. Fastly has not yet adopted this crate, so the
# Trusted Server maintainers own it until a vendor owner steps up.
[package.metadata.maintainers]
owner = "Trusted Server maintainers"
status = "seeking vendor owner"
98 changes: 98 additions & 0 deletions crates/device/fastly/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! The Fastly device module and host-signal capture.
//!
//! [`FastlyDeviceModule`] strengthens the built-in User-Agent classification
//! with the host's TLS (JA4) and HTTP/2 signals, for deployments on Fastly
//! Compute. It is selected by `[device] module = "fastly"` and wired in by the
//! Fastly adapter, which injects the request info and the captured host signals.
//!
//! [`FastlyHostSignals`] captures those signals from a live Fastly request
//! (`get_tls_ja4()`, `get_client_h2_fingerprint()`) into owned values, so it can
//! be shared as an injected [`HostSignals`] service that outlives the borrow of
//! the request. Capturing through the SDK is why this crate depends on the
//! `fastly` crate and builds only for the `wasm32-wasip1` target; off-host the
//! accessors return `None`, so classification degrades to User-Agent only. The
//! platform-neutral [`HostSignals`], [`RequestInfo`], and [`DeviceModule`]
//! traits and the built-in default live in `trusted-server-core`, where the
//! `DeviceSignals` classification logic stays unit-tested.

use std::sync::Arc;

use fastly::Request as FastlyRequest;
use trusted_server_core::ec::device::{DeviceModule, DeviceSignals};
use trusted_server_core::evidence::{HostSignals, RequestInfo};

/// Host-computed client signals captured from a live Fastly request.
///
/// Reads the TLS JA4 and HTTP/2 signals once through the Fastly SDK and
/// owns them, so the value can be injected as a [`HostSignals`] service that
/// outlives the borrow of the request it was captured from. Off-host the SDK
/// accessors return `None`, so the signals are simply absent.
#[derive(Debug, Clone, Default)]
pub struct FastlyHostSignals {
ja4: Option<String>,
h2: Option<String>,
}

impl FastlyHostSignals {
/// Builds host signals from already-captured signal values.
///
/// Use this when the adapter has read the signals once (for example
/// into the client metadata, or from the trusted internal headers the entry
/// point injects) and wants to share them without another SDK call.
#[must_use]
pub fn new(ja4: Option<String>, h2: Option<String>) -> Self {
Self { ja4, h2 }
}

/// Captures the TLS JA4 and HTTP/2 signals from a live Fastly request.
#[must_use]
pub fn from_request(req: &FastlyRequest) -> Self {
Self {
ja4: req.get_tls_ja4().map(str::to_string),
h2: req.get_client_h2_fingerprint().map(str::to_string),
}
}
}

impl HostSignals for FastlyHostSignals {
fn ja4(&self) -> Option<&str> {
self.ja4.as_deref()
}

fn h2(&self) -> Option<&str> {
self.h2.as_deref()
}
}

/// The Fastly device module, opt-in via `[device] module = "fastly"`.
///
/// Classifies a request with [`DeviceSignals::derive`], which strengthens the
/// User-Agent classification with the host signals. It reads the User-Agent
/// from its injected [`RequestInfo`] and the TLS and HTTP/2 signals from its
/// injected [`HostSignals`], so the browser/bot gate is backed by the live
/// request.
pub struct FastlyDeviceModule {
host_signals: Arc<dyn HostSignals>,
}

impl FastlyDeviceModule {
/// Creates the module with its injected host signals.
#[must_use]
pub fn new(host_signals: Arc<dyn HostSignals>) -> Self {
Self { host_signals }
}
}

impl DeviceModule for FastlyDeviceModule {
fn id(&self) -> &'static str {
"fastly"
}

fn detect(&self, request_info: &dyn RequestInfo) -> DeviceSignals {
DeviceSignals::derive(
request_info.user_agent(),
self.host_signals.ja4(),
self.host_signals.h2(),
)
}
}
18 changes: 18 additions & 0 deletions crates/edgecookie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Edge Cookie modules

Vendor Edge Cookie module crates live here, one per vendor, for example
`crates/edgecookie/<vendor>`. Each implements the `EdgeCookieModule` trait
from `trusted-server-core` and is wired in by an adapter.

The built-in HMAC module (HMAC over the client IP) ships in
`trusted-server-core` (`ec::module`), so no crate is needed for it. There is
no default module, and a deployment selects one explicitly with
`[ec] module`.

A module's own settings live in the `[ec.<name>]` table the selector names.
The name is the module's implementation id, the same string its
`EdgeCookieModule::id` returns, unless the table names one with
`implementation = "<id>"`, which lets an operator configure a module under a
name of their own choosing. A module with no settings needs no table.

This directory is a placeholder until a vendor module is added.
13 changes: 13 additions & 0 deletions crates/fastly.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Minimal Viceroy config for testing crates nested one level deeper than the
# adapters (for example `crates/device/fastly` and `crates/geo/fastly`).
#
# The shared wasm test runner in `.cargo/config.toml` starts Viceroy with
# `-C ../../fastly.toml`, resolved from the crate directory. For a two-level
# crate such as `crates/trusted-server-adapter-fastly` that reaches the
# repository root manifest. For a three-level crate it resolves here, to
# `crates/fastly.toml`. These crates' unit tests use no backends, KV stores,
# or dictionaries, only a manifest Viceroy can start from.
manifest_version = 3
name = "trusted-server-nested-crate-tests"

[local_server]
20 changes: 20 additions & 0 deletions crates/geo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Geo modules

Geo and IP-intelligence module crates live here, one per implementation, each
implementing the `PlatformGeo` trait from `trusted-server-core`:

- `crates/geo/fastly` (`trusted-server-geo-fastly`) is the host platform geo
module for Fastly Compute, wrapping Fastly's `geo_lookup`. The Fastly adapter
injects it via `build_geo_module`. It depends on the Fastly SDK, so it builds
only for `wasm32-wasip1`.
- Vendor geo modules (for example `crates/geo/<vendor>`) will live alongside
it, one per vendor, selected by the `[geo] module` setting.

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.

The platform-neutral `PlatformGeo` trait and the `DisabledGeo` default (no
location) both live in `trusted-server-core`, so the default deployment resolves
no location until a module is selected.
26 changes: 26 additions & 0 deletions crates/geo/fastly/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "trusted-server-geo-fastly"
description = "Fastly host geo module backed by the Fastly geolocation API."
authors = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
publish = { workspace = true }
version = { workspace = true }

[lib]
doctest = false

[lints]
workspace = true

[dependencies]
trusted-server-core = { workspace = true }
error-stack = { workspace = true }
fastly = { workspace = true }

# The visible owner of this vendor crate, the way Prebid.js requires a named
# maintainer of every adapter. Fastly has not yet adopted this crate, so the
# Trusted Server maintainers own it until a vendor owner steps up.
[package.metadata.maintainers]
owner = "Trusted Server maintainers"
status = "seeking vendor owner"
47 changes: 47 additions & 0 deletions crates/geo/fastly/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! The Fastly host geo module.
//!
//! [`FastlyPlatformGeo`] implements [`PlatformGeo`] using Fastly's `geo_lookup`,
//! for deployments on Fastly Compute. It is the host platform's geo module,
//! injected by the Fastly adapter via `build_geo_module`. With no selector,
//! or `module = "platform"`, this host lookup resolves the location, and
//! `module = "none"` disables geo instead.
//!
//! Like the Fastly device module, this crate calls the Fastly SDK directly,
//! so it depends on the `fastly` crate and builds only for the `wasm32-wasip1`
//! target. The platform-neutral `PlatformGeo` trait and the `DisabledGeo`
//! default both live in `trusted-server-core`.

use std::net::IpAddr;

use error_stack::Report;
use fastly::geo::{Geo, geo_lookup};
use trusted_server_core::platform::{GeoInfo, PlatformError, PlatformGeo};

/// Convert a Fastly [`Geo`] value into a platform-neutral [`GeoInfo`].
fn geo_from_fastly(geo: &Geo) -> GeoInfo {
GeoInfo {
city: geo.city().to_string(),
country: geo.country_code().to_string(),
continent: format!("{:?}", geo.continent()),
latitude: geo.latitude(),
longitude: geo.longitude(),
metro_code: geo.metro_code(),
region: geo.region().map(str::to_string),
asn: None,
}
}

/// Fastly geo-lookup implementation of [`PlatformGeo`].
///
/// The host platform geo module for Fastly Compute. The adapter injects it via
/// `build_geo_module`. With no selector, or `module = "platform"`, it
/// resolves the location, and `module = "none"` disables geo instead.
pub struct FastlyPlatformGeo;

impl PlatformGeo for FastlyPlatformGeo {
fn lookup(&self, client_ip: Option<IpAddr>) -> Result<Option<GeoInfo>, Report<PlatformError>> {
Ok(client_ip
.and_then(geo_lookup)
.map(|geo| geo_from_fastly(&geo)))
}
}
Loading
Loading