Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
13d432c
Add a pluggable Edge Cookie provider seam with the built-in HMAC prov…
jwrosewell Aug 18, 2026
ea51812
Accept the provider-code envelope on the partner-facing identifier paths
jwrosewell Aug 27, 2026
1f4a70b
Rename the legacy passphrase migration so CodeQL stops tainting Settings
jwrosewell Aug 27, 2026
f0ca12a
Stop serving without identity when a selected provider is unavailable
jwrosewell Aug 28, 2026
b2bb944
Hold the deprecated EC passphrase to the same rules as the new block
jwrosewell Aug 28, 2026
472218b
Reject unknown keys in the built-in HMAC provider block
jwrosewell Aug 28, 2026
c4d2f1d
Stop rather than run stateless when the hmac block is missing
jwrosewell Aug 28, 2026
93cd1e8
Restore the missing line continuation in the mint rejection message
jwrosewell Aug 28, 2026
a265c96
Give EdgeCookieProvider its own doc comment back
jwrosewell Aug 28, 2026
004581c
Delete the unused ec::get_ec_id helper
jwrosewell Aug 28, 2026
d6041f0
Correct the provider module docs about when evidence arrives
jwrosewell Aug 28, 2026
885e3ce
Replace the scattered EC provider key strings with a typed selector
jwrosewell Aug 28, 2026
69649aa
Reserve core's own response surface against provider effects
jwrosewell Aug 29, 2026
53d632e
Dispatch partner-path identifier checks by provider code
jwrosewell Aug 29, 2026
941297f
Let each provider decide whether it needs the client IP
jwrosewell Aug 29, 2026
e317190
State a real retirement condition for the legacy bare-identifier reader
jwrosewell Aug 29, 2026
343ac3e
Key identity-graph reads and write-backs by the canonical form
jwrosewell Aug 29, 2026
8684c69
Egress only an Edge Cookie identifier the provider recognizes
jwrosewell Aug 29, 2026
20bb082
Record the cluster-count gap the identifier envelope opens
jwrosewell Aug 29, 2026
84925ca
Consume the refused Report in the testlight egress tests
jwrosewell Aug 29, 2026
e45990b
Drop the request-evidence accessors that have no caller
jwrosewell Aug 29, 2026
b146aeb
Collapse the EC provider selector to statelessness and a named provider
jwrosewell Aug 29, 2026
7c0428d
Accumulate provider response headers instead of replacing the origin's
jwrosewell Aug 29, 2026
69d5ed0
State what a provider switch really does to existing identities
jwrosewell Aug 29, 2026
60a1f4b
Name the design documents rather than their paths in doc comments
jwrosewell Aug 30, 2026
ada4d79
Restore the line continuations missed in the neighbouring files
jwrosewell Aug 30, 2026
4cf202f
Correct the two provider doc comments the earlier pass missed
jwrosewell Aug 30, 2026
abdd4cb
Stop a provider code from panicking when a vendor builds one at run time
jwrosewell Aug 30, 2026
138dc3a
Refuse two Edge Cookie providers claiming the same name
jwrosewell Aug 30, 2026
9f4061a
Build the internal header list from the Edge Cookie response headers
jwrosewell Aug 30, 2026
0f0722f
Resolve the Edge Cookie provider once per request instead of twice
jwrosewell Aug 30, 2026
a698f5b
Load Spin settings from the config store instead of a baked template
jwrosewell Aug 30, 2026
252aade
Stop exposing an inbound Edge Cookie identifier nothing has vouched for
jwrosewell Aug 30, 2026
6cc3c91
Keep the whole request-evidence interface
jwrosewell Aug 30, 2026
5d87e24
Documentation and house-style cleanup for the Edge Cookie provider PR
jwrosewell Aug 31, 2026
11cc575
Name the caching reason accurately in the reserved-header rejection
jwrosewell Aug 31, 2026
9c50e04
Add device and geo provider selection with the host-signal Edge Cooki…
jwrosewell Aug 19, 2026
b1fba32
Omit an unset provider selector from the serialized config
jwrosewell Aug 30, 2026
8a275df
Read the Cloudflare visitor region so US state opt-outs are honored
jwrosewell Aug 30, 2026
2e37581
Reunite the request-info builders with their own documentation
jwrosewell Aug 31, 2026
1371127
Documentation and house-style cleanup for the device and geo provider PR
jwrosewell Aug 31, 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"

# --- Axum adapter (native dev server) ---
build-axum = "build -p trusted-server-adapter-axum"
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 @@ -109,6 +111,8 @@ toml = "1.1"
toml_edit = "0.23.10"
tower = "0.4"
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 providers

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

The built-in default provider (User-Agent only) ships in `trusted-server-core`
(`ec::device`). Adapters select and inject the vendor provider via
`build_device_provider`.
18 changes: 18 additions & 0 deletions crates/device/fastly/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "trusted-server-device-fastly"
description = "Fastly host device provider 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 }
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 provider and host-signal capture.
//!
//! [`FastlyDeviceProvider`] 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] provider = "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 [`DeviceProvider`]
//! 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::{DeviceProvider, 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 provider, opt-in via `[device] provider = "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 FastlyDeviceProvider {
host_signals: Arc<dyn HostSignals>,
}

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

impl DeviceProvider for FastlyDeviceProvider {
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(),
)
}
}
10 changes: 10 additions & 0 deletions crates/edgecookie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Edge Cookie providers

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

The built-in HMAC provider (HMAC over the client IP) ships in
`trusted-server-core` (`ec::provider`), so no crate is needed for it. There is
no default provider; a deployment selects one explicitly with `[ec] provider`.
This directory is a placeholder until a vendor provider 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 providers

Geo and IP-intelligence provider 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
provider for Fastly Compute, wrapping Fastly's `geo_lookup`. The Fastly adapter
injects it via `build_geo_provider`. It depends on the Fastly SDK, so it builds
only for `wasm32-wasip1`.
- Vendor geo providers (for example `crates/geo/<vendor>`) will live alongside
it, one per vendor, selected by the `[geo] provider` setting.

Whatever the source, a provider 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 providers 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 provider is selected.
19 changes: 19 additions & 0 deletions crates/geo/fastly/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "trusted-server-geo-fastly"
description = "Fastly host geo provider 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 }
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 provider.
//!
//! [`FastlyPlatformGeo`] implements [`PlatformGeo`] using Fastly's `geo_lookup`,
//! for deployments on Fastly Compute. It is the host platform's geo provider,
//! injected by the Fastly adapter via `build_geo_provider`. With no selector,
//! or `provider = "platform"`, this host lookup resolves the location, and
//! `provider = "none"` disables geo instead.
//!
//! Like the Fastly device provider, 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 provider for Fastly Compute. The adapter injects it via
/// `build_geo_provider`. With no selector, or `provider = "platform"`, it
/// resolves the location, and `provider = "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