Skip to content
Draft
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
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
## Project Overview

Rust-based edge computing application targeting **Fastly Compute**. Handles
privacy-preserving Edge Cookie (EC) ID generation, ad serving with GDPR compliance,
real-time bidding integration, and publisher-side JavaScript injection.
Edge Cookie (EC) ID generation, ad serving with consent signal extraction
and enforcement, real-time bidding integration, and publisher-side
JavaScript injection.

## Workspace Layout

Expand Down
2 changes: 1 addition & 1 deletion crates/js/lib/src/core/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import NORMALIZE_CSS from './styles/normalize.css?inline';
import IFRAME_TEMPLATE from './templates/iframe.html?raw';

// Sandbox permissions granted to creative iframes.
// Ad creatives routinely contain scripts for tracking, click handling, and
// Ad creatives routinely contain scripts for impression reporting, click handling, and
// viewability measurement, so allow-scripts and allow-same-origin are required
// for creatives to render correctly. Server-side sanitization is the primary
// defense against malicious markup; the sandbox provides defense-in-depth.
Expand Down
2 changes: 1 addition & 1 deletion crates/js/lib/src/integrations/creative/click.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Click guard runtime: detects mutated tracking URLs and rebuilds signed first-party clicks.
// Click guard runtime: detects mutated click URLs and rebuilds signed first-party clicks.
import { log } from '../../core/log';
import { creativeGlobal } from '../../shared/globals';
import { delay, queueTask } from '../../shared/async';
Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ mod tests {
);
assert!(
body.contains("h2_fp: unavailable"),
"should include H2 fingerprint fallback"
"should include H2 probabilistic identifier fallback"
);
assert!(
body.contains("cipher: unavailable"),
Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/auction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ The orchestrator collects all bids and creates an OpenRTB response:
}
```

Note that creative HTML is rewritten to use the first-party proxy (`/first-party/proxy`) for privacy and security.
Note that creative HTML is rewritten to use the first-party proxy (`/first-party/proxy`) for security.

## Route Registration & Endpoints

Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-core/src/auction/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct AuctionRequest {
pub slots: Vec<AdSlot>,
/// Publisher information
pub publisher: PublisherInfo,
/// User information (privacy-preserving)
/// User information (consent-aware)
pub user: UserInfo,
/// Device information
pub device: Option<DeviceInfo>,
Expand Down Expand Up @@ -67,7 +67,7 @@ pub struct PublisherInfo {
pub page_url: Option<String>,
}

/// Privacy-preserving user information.
/// Consent-aware user information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
/// Stable EC ID (from cookie or freshly generated).
Expand Down
6 changes: 3 additions & 3 deletions crates/trusted-server-core/src/consent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ impl ConsentForwardingMode {
/// The `applies_in` list is used for **observability and logging only** — it
/// does NOT cause consent to be synthesized. When a user's country appears in
/// this list, the system logs that GDPR applies, enabling publishers to
/// monitor compliance coverage.
/// monitor jurisdiction coverage.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GdprConfig {
/// ISO 3166-1 alpha-2 country codes where GDPR applies.
Expand Down Expand Up @@ -272,11 +272,11 @@ impl Default for ConflictResolutionConfig {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ConflictMode {
/// Deny consent when signals disagree (most privacy-safe).
/// Deny consent when signals disagree (most restrictive).
Restrictive,
/// Use the newer signal based on timestamps.
Newest,
/// Grant consent when signals disagree (requires legal review).
/// Grant consent when signals disagree (least restrictive).
Permissive,
}

Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub const HEADER_REFERER: HeaderName = HeaderName::from_static("referer");
///
/// These headers are used internally by Trusted Server for identity, geo-enrichment,
/// debugging, and compression hints. Leaking them to external origins could expose
/// user tracking data and internal implementation details.
/// user identity, geo data, and internal implementation details.
///
/// Uses `&str` slices because `HeaderName` has interior mutability and cannot appear
/// in `const` context.
Expand Down
26 changes: 13 additions & 13 deletions crates/trusted-server-core/src/ec/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub struct DeviceSignals {
/// Coarse OS family: `"mac"`, `"windows"`, `"ios"`, `"android"`,
/// `"linux"`.
pub platform_class: Option<String>,
/// SHA256 prefix (12 hex chars) of raw H2 SETTINGS fingerprint.
/// SHA256 prefix (12 hex chars) of the raw H2 SETTINGS string.
pub h2_fp_hash: Option<String>,
/// `true` = known browser, `false` = known bot, `None` = unknown.
pub known_browser: Option<bool>,
Expand Down Expand Up @@ -65,8 +65,8 @@ impl DeviceSignals {
/// Returns `true` when the request looks like a real browser.
///
/// Checks for the presence of recognizable signals rather than matching
/// against a hardcoded fingerprint allowlist. Real browsers always
/// produce a valid TLS fingerprint (`ja4_class`) and a recognizable UA
/// against a hardcoded signal allowlist. Real browsers always
/// produce a valid TLS probabilistic identifier (`ja4_class`) and a recognizable UA
/// platform string (`platform_class`). Raw HTTP clients (curl, Python
/// requests, Go net/http, headless scrapers) typically lack one or both.
///
Expand Down Expand Up @@ -146,11 +146,11 @@ fn parse_platform_class(ua: &str) -> Option<String> {
None
}

/// Extracts Section 1 from a full JA4 fingerprint.
/// Extracts Section 1 from a full JA4 string.
///
/// JA4 format: `section1_section2_section3` separated by underscores.
/// Section 1 identifies browser family (cipher count, extension count,
/// ALPN) without uniquely fingerprinting a device.
/// ALPN) without uniquely identifying a device.
///
/// Returns `None` if the input is empty or has no underscore-delimited
/// section.
Expand All @@ -164,7 +164,7 @@ fn extract_ja4_section1(full_ja4: &str) -> Option<String> {
}

/// Computes a 12-hex-char prefix of the SHA256 hash of the raw H2
/// SETTINGS fingerprint string.
/// SETTINGS string.
///
/// The raw string looks like `"1:65536;2:0;4:6291456;6:262144"`.
#[must_use]
Expand All @@ -175,7 +175,7 @@ fn compute_h2_fp_hash(raw_h2_fp: &str) -> String {
hex::encode(&digest[..6])
}

/// Known browser fingerprint allowlist.
/// Known browser signal allowlist.
///
/// Each entry is `(ja4_class, h2_fp_prefix, known_browser)`.
/// `h2_fp_prefix` is the raw H2 SETTINGS string (not the hash) — we
Expand All @@ -191,7 +191,7 @@ const KNOWN_BROWSERS: &[(&str, &str, bool)] = &[
("t13d1717h2", "1:65536;2:0;4:131072;5:16384", true),
];

/// Returns H2 fingerprint hashes for the known browser allowlist.
/// Returns H2 SETTINGS hashes for the known browser allowlist.
///
/// Computed once on first call and cached via `OnceLock`.
fn known_browser_h2_hashes() -> &'static Vec<(&'static str, String, bool)> {
Expand Down Expand Up @@ -366,7 +366,7 @@ mod tests {
assert_eq!(
evaluate_known_browser(Some(ja4), Some(&h2_hash)),
Some(true),
"Chrome fingerprint should be recognized"
"Chrome signals should be recognized"
);
}

Expand All @@ -377,7 +377,7 @@ mod tests {
assert_eq!(
evaluate_known_browser(Some(ja4), Some(&h2_hash)),
Some(true),
"Safari fingerprint should be recognized"
"Safari signals should be recognized"
);
}

Expand All @@ -388,7 +388,7 @@ mod tests {
assert_eq!(
evaluate_known_browser(Some(ja4), Some(&h2_hash)),
Some(true),
"Firefox fingerprint should be recognized"
"Firefox signals should be recognized"
);
}

Expand Down Expand Up @@ -538,7 +538,7 @@ mod tests {
);
assert!(
signals.looks_like_browser(),
"unknown fingerprint with valid JA4 + platform should pass"
"unknown signal combination with valid JA4 + platform should pass"
);
assert_eq!(signals.known_browser, None, "should not match allowlist");
}
Expand All @@ -554,7 +554,7 @@ mod tests {

#[test]
fn looks_like_browser_rejects_missing_ja4() {
// Real UA but no TLS fingerprint (e.g. HTTP/1.1 or missing SDK support)
// Real UA but no JA4 value (e.g. HTTP/1.1 or missing SDK support)
let signals = DeviceSignals::derive(CHROME_MAC_UA, None, Some("1:65536"));
assert!(
!signals.looks_like_browser(),
Expand Down
10 changes: 5 additions & 5 deletions crates/trusted-server-core/src/ec/generation.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Edge Cookie (EC) ID generation using HMAC.
//!
//! This module provides functionality for generating privacy-preserving EC IDs
//! based on the client IP address and a secret key.
//! This module generates EC IDs from the client IP address and a configured
//! secret key.

use std::net::IpAddr;

Expand Down Expand Up @@ -65,9 +65,9 @@ fn generate_random_suffix(length: usize) -> String {
/// Generates a fresh EC ID from a pre-captured client IP string.
///
/// Uses only the client IP (not user-agent or other headers) intentionally:
/// EC IDs are meant to be simple, privacy-preserving identifiers — not
/// high-entropy fingerprints. The random suffix provides per-cookie
/// uniqueness for users behind the same NAT/proxy.
/// EC IDs are deterministic identifiers (HMAC base plus random suffix),
/// not high-entropy probabilistic identifiers. The random suffix provides
/// per-cookie uniqueness for users behind the same NAT or proxy.
///
/// Creates an HMAC-SHA256-based ID using the configured secret key and
/// the client IP address, then appends a random suffix for additional
Expand Down
12 changes: 6 additions & 6 deletions crates/trusted-server-core/src/ec/kv_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub struct KvEntry {
/// Creation-time publisher property metadata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pub_properties: Option<KvPubProperties>,
/// Device class signals (TLS fingerprint, UA platform).
/// Device class signals (TLS handshake, UA platform).
/// Written once on creation — never updated after.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub device: Option<KvDevice>,
Expand Down Expand Up @@ -92,7 +92,7 @@ pub struct KvGeo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub asn: Option<u32>,
/// DMA/metro code (e.g. `807` = San Francisco).
/// Market-level targeting signal; not personal data.
/// Market-level targeting signal.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dma: Option<i64>,
}
Expand Down Expand Up @@ -143,15 +143,15 @@ where
}
}

/// Coarse, non-PII device signals derived from TLS handshake and UA.
/// Coarse device signals derived from TLS handshake and UA.
///
/// Used by the `/_ts/api/v1/identify` endpoint for cross-suffix propagation decisions
/// and buyer-facing device quality scoring. Written once on
/// [`KvEntry`] creation — never updated after.
///
/// **Privacy:** `ja4_class` (Section 1 only) and `platform_class` are
/// category signals, not unique device identifiers. The full JA4
/// fingerprint (Sections 2–3) is never stored.
/// string (Sections 2–3) is never stored.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KvDevice {
/// Mobile signal: `0` = confirmed desktop, `1` = confirmed mobile,
Expand All @@ -160,14 +160,14 @@ pub struct KvDevice {
pub is_mobile: u8,
/// JA4 Section 1 only — browser family class identifier.
/// e.g. `"t13d1516h2"` = Chrome, `"t13d2013h2"` = Safari.
/// Never stores the full JA4 fingerprint.
/// Never stores the full JA4 string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ja4_class: Option<String>,
/// Coarse OS family from UA: `"mac"`, `"windows"`, `"ios"`,
/// `"android"`, `"linux"`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform_class: Option<String>,
/// SHA256 prefix (12 hex chars) of the HTTP/2 SETTINGS fingerprint.
/// SHA256 prefix (12 hex chars) of the HTTP/2 SETTINGS string.
/// Used alongside `ja4_class` for browser confirmation and bot detection.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub h2_fp_hash: Option<String>,
Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/ec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
//! - [`cookies`] — `Set-Cookie` header creation and expiration helpers
//! - [`kv`] — KV Store identity graph operations (CAS, tombstones, debounce)
//! - [`kv_types`] — Schema types for KV identity graph entries
//! - [`device`]Device signal derivation (UA, JA4, H2 fingerprinting)
//! - [`device`]: Device signal derivation (UA, JA4, H2 SETTINGS)
//! - [`partner`] — Partner validation helpers (ID format, pull sync config)
//! - [`registry`] — In-memory partner registry built from config
//! - [`rate_limiter`] — Rate limiting abstraction (Fastly Edge Rate Limiting)
Expand Down
6 changes: 3 additions & 3 deletions crates/trusted-server-core/src/integrations/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ struct ApsContextual {
#[serde(default)]
slots: Vec<ApsSlotResponse>,

/// Event tracking host
/// Event collection endpoint
#[serde(skip_serializing_if = "Option::is_none")]
host: Option<String>,

Expand All @@ -111,7 +111,7 @@ struct ApsContextual {
#[serde(skip_serializing_if = "Option::is_none")]
cfe: Option<bool>,

/// Event tracking enabled
/// Event collection enabled
#[serde(skip_serializing_if = "Option::is_none")]
ev: Option<bool>,

Expand All @@ -123,7 +123,7 @@ struct ApsContextual {
#[serde(skip_serializing_if = "Option::is_none")]
cb: Option<String>,

/// Campaign tracking URL
/// Campaign attribution URL
#[serde(skip_serializing_if = "Option::is_none")]
cmp: Option<String>,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Google Tag Manager integration for first-party tag delivery.
//!
//! Proxies GTM scripts and Google Analytics beacons through the publisher's
//! domain, improving tracking accuracy and ad-blocker resistance.
//! domain, improving measurement accuracy and ad-blocker resistance.
//!
//! # Endpoints
//!
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-core/src/integrations/gpt.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Google Publisher Tags (GPT) integration for first-party ad serving.
//!
//! This module provides transparent proxying for Google's entire GPT script
//! chain, enabling first-party ad tag delivery while maintaining privacy
//! controls. GPT loads scripts in a cascade:
//! chain, enabling first-party ad tag delivery.
//! GPT loads scripts in a cascade:
//!
//! 1. `gpt.js` – the thin bootstrap loader
//! 2. `pubads_impl.js` – the main GPT implementation (~640 KB)
Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/integrations/lockr.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Lockr integration for identity resolution and advertising tokens.
//!
//! This module provides transparent proxying for Lockr's SDK and API,
//! enabling first-party identity resolution while maintaining privacy controls.
//! enabling first-party identity resolution.
//!
//! Lockr provides a dedicated trust-server SDK (`identity-lockr-trust-server.js`)
//! that is pre-configured to route API calls through the first-party proxy,
Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/integrations/permutive.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Permutive integration for first-party data collection and audience management.
//!
//! This module provides transparent proxying for Permutive's API and SDK,
//! enabling first-party data collection while maintaining privacy controls.
//! enabling first-party data collection.
use std::sync::Arc;

Expand Down
2 changes: 1 addition & 1 deletion crates/trusted-server-core/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ fn finalize_proxied_response(
beresp.set_header(header::CONTENT_TYPE, "image/*");
}

// Heuristics to log likely tracking pixels without altering response
// Heuristics to log likely pixel images without altering response
let mut is_pixel = false;
if let Some(cl) = beresp
.get_header(header::CONTENT_LENGTH)
Expand Down
4 changes: 2 additions & 2 deletions crates/trusted-server-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1644,10 +1644,10 @@ impl Proxy {
/// Debug-only features. All flags default to `false` (off in production).
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct DebugConfig {
/// Expose the JA4/TLS fingerprint debug endpoint at `GET /_ts/debug/ja4`.
/// Expose the JA4/TLS probabilistic identifier debug endpoint at `GET /_ts/debug/ja4`.
///
/// When `false` (the default), the endpoint returns 404. Enable only for
/// intentional Fastly/browser TLS investigation — the endpoint reflects
/// intentional Fastly/browser TLS investigation. The endpoint reflects
/// Fastly-observed TLS details that browser JS cannot normally read.
#[serde(default)]
pub ja4_endpoint_enabled: bool,
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default withMermaid(
defineConfig({
title: 'Trusted Server',
description:
'Privacy-preserving edge computing for ad serving and edge cookie (EC) generation',
'Edge computing for ad serving, consent signal handling, and edge cookie (EC) generation',
base: '/trusted-server',

// Replace version placeholders like {{NODEJS_VERSION}} with values from .tool-versions
Expand Down
Loading