Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -107,6 +109,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 fingerprints; 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 }
97 changes: 97 additions & 0 deletions crates/device/fastly/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
//! 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 fingerprints, 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 fingerprints 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 fingerprints captured from a live Fastly request.
///
/// Reads the TLS JA4 and HTTP/2 fingerprints 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 fingerprint values.
///
/// Use this when the adapter has read the fingerprints 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 fingerprints 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 the fingerprint-strengthened
/// [`DeviceSignals::derive`], reading the User-Agent from its injected
/// [`RequestInfo`] and the TLS/HTTP-2 fingerprints 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(),
)
}
}
9 changes: 9 additions & 0 deletions crates/edgecookie/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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 default provider (HMAC over the client IP) ships in
`trusted-server-core` (`ec::provider`), so no crate is needed for it. 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 }
45 changes: 45 additions & 0 deletions crates/geo/fastly/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! 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`; selecting a vendor
//! geo provider replaces it.
//!
//! Unlike the pure-logic device provider, this crate calls the Fastly geo 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`; selecting a vendor geo provider replaces it.
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)))
}
}
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-axum/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ mod tests {
proxy_secret = "unit-test-proxy-secret"

[ec]
provider = "hmac"

[ec.providers.hmac]
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-axum/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ fn test_router() -> edgezero_core::router::RouterService {
proxy_secret = "integration-test-proxy-secret"

[ec]
provider = "hmac"

[ec.providers.hmac]
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
Expand Down
3 changes: 3 additions & 0 deletions crates/trusted-server-adapter-cloudflare/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ mod tests {
proxy_secret = "unit-test-proxy-secret"

[ec]
provider = "hmac"

[ec.providers.hmac]
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
Expand Down
6 changes: 6 additions & 0 deletions crates/trusted-server-adapter-cloudflare/tests/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ fn test_router() -> RouterService {
proxy_secret = "route-test-proxy-secret"

[ec]
provider = "hmac"

[ec.providers.hmac]
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
Expand Down Expand Up @@ -85,6 +88,9 @@ fn make_router() -> RouterService {
proxy_secret = "integration-test-proxy-secret"

[ec]
provider = "hmac"

[ec.providers.hmac]
passphrase = "test-secret-key-32-bytes-minimum"
"#,
)
Expand Down
2 changes: 2 additions & 0 deletions crates/trusted-server-adapter-fastly/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
trusted-server-core = { workspace = true }
trusted-server-device-fastly = { workspace = true }
trusted-server-geo-fastly = { workspace = true }
url = { workspace = true }
urlencoding = { workspace = true }

Expand Down
Loading
Loading