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
3 changes: 1 addition & 2 deletions crates/integration-tests/tests/frameworks/scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,7 @@ impl CustomScenario {
///
/// These run against the Viceroy runtime directly without a frontend
/// framework container β€” they exercise EC-specific endpoints
/// (`/_ts/api/v1/sync`, `/_ts/api/v1/identify`,
/// `/_ts/api/v1/batch-sync`, `/_ts/admin/v1/partners/register`).
/// (`/_ts/api/v1/identify`, `/_ts/api/v1/batch-sync`).
#[derive(Debug, Clone)]
pub enum EcScenario {
/// Seeded EC row β†’ batch sync writes partner UID β†’ identify (Bearer auth)
Expand Down
11 changes: 9 additions & 2 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use trusted_server_core::ec::registry::PartnerRegistry;
use trusted_server_core::ec::EcContext;
use trusted_server_core::error::TrustedServerError;
use trusted_server_core::geo::GeoInfo;
use trusted_server_core::integrations::IntegrationRegistry;
use trusted_server_core::integrations::{IntegrationRegistry, ProxyDispatchInput};
use trusted_server_core::platform::RuntimeServices;
use trusted_server_core::proxy::{
handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild,
Expand Down Expand Up @@ -401,7 +401,14 @@ async fn route_request(
}
(m, path) if integration_registry.has_route(&m, path) => {
let result = integration_registry
.handle_proxy(&m, path, settings, kv_graph.as_ref(), &mut ec_context, req)
.handle_proxy(ProxyDispatchInput {
method: &m,
path,
settings,
kv: kv_graph.as_ref(),
ec_context: &mut ec_context,
req,
})
.await
.unwrap_or_else(|| {
Err(Report::new(TrustedServerError::BadRequest {
Expand Down
29 changes: 24 additions & 5 deletions crates/trusted-server-core/src/auction/endpoints.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! HTTP endpoint handlers for auction requests.

use error_stack::{Report, ResultExt};
use fastly::http::StatusCode;
use fastly::{Request, Response};
use serde_json::Value as JsonValue;

Expand All @@ -27,6 +28,12 @@ const MAX_CLIENT_EID_SOURCES: usize = 64;
const MAX_CLIENT_UIDS_PER_SOURCE: usize = 32;
const MAX_CLIENT_EID_SOURCE_BYTES: usize = 255;

/// Maximum accepted JSON body size for `/auction`. Picked to comfortably fit
/// the largest realistic Prebid-derived auction request (hundreds of ad units
/// with EID arrays) while preventing an authenticated client from consuming
/// arbitrary WASM linear memory.
const MAX_AUCTION_BODY_SIZE: usize = 256 * 1024;

/// Handle auction request from /auction endpoint.
///
/// This is the main entry point for running header bidding auctions.
Expand All @@ -49,12 +56,24 @@ pub async fn handle_auction(
services: &RuntimeServices,
mut req: Request,
) -> Result<Response, Report<TrustedServerError>> {
// Parse request body
let body: AdRequest = serde_json::from_slice(&req.take_body_bytes()).change_context(
TrustedServerError::Auction {
// Reject oversized bodies before any allocation. The `Content-Length`
// pre-check stops well-behaved clients early; the post-read check defends
// against clients that lie about (or omit) the header.
let content_length_exceeded = req
.get_header_str("content-length")
.and_then(|value| value.parse::<usize>().ok())
.is_some_and(|length| length > MAX_AUCTION_BODY_SIZE);
if content_length_exceeded {
return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE));
}
let body_bytes = req.take_body_bytes();
if body_bytes.len() > MAX_AUCTION_BODY_SIZE {
return Ok(Response::from_status(StatusCode::PAYLOAD_TOO_LARGE));
}
let body: AdRequest =
serde_json::from_slice(&body_bytes).change_context(TrustedServerError::Auction {
message: "Failed to parse auction request body".to_string(),
},
)?;
})?;

log::info!(
"Auction request received for {} ad units",
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 @@ -334,7 +334,7 @@ impl EcContext {

/// Returns a mutable reference to the consent context.
///
/// Used by `/_ts/api/v1/sync` to apply query-param fallback consent for the current
/// Allows handlers to apply query-param fallback consent for the current
/// request only when pre-routing consent extraction produced an empty
/// context.
pub fn consent_mut(&mut self) -> &mut ConsentContext {
Expand Down
3 changes: 2 additions & 1 deletion crates/trusted-server-core/src/integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ pub use registry::{
IntegrationAttributeRewriter, IntegrationDocumentState, IntegrationEndpoint,
IntegrationHeadInjector, IntegrationHtmlContext, IntegrationHtmlPostProcessor,
IntegrationMetadata, IntegrationProxy, IntegrationRegistration, IntegrationRegistrationBuilder,
IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ScriptRewriteAction,
IntegrationRegistry, IntegrationScriptContext, IntegrationScriptRewriter, ProxyDispatchInput,
ScriptRewriteAction,
};

type IntegrationBuilder =
Expand Down
86 changes: 51 additions & 35 deletions crates/trusted-server-core/src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,20 @@ impl IntegrationMetadata {
}
}

/// Inputs to [`IntegrationRegistry::handle_proxy`].
///
/// Bundled into a struct so the dispatch surface stays within the project's
/// 7-argument cap; `ec_context` and `req` participate in the borrow so the
/// whole thing shares one lifetime.
pub struct ProxyDispatchInput<'a> {
pub method: &'a Method,
pub path: &'a str,
pub settings: &'a Settings,
pub kv: Option<&'a KvIdentityGraph>,
pub ec_context: &'a mut EcContext,
pub req: Request,
}

/// In-memory registry of integrations discovered from settings.
#[derive(Clone, Default)]
pub struct IntegrationRegistry {
Expand Down Expand Up @@ -656,17 +670,19 @@ impl IntegrationRegistry {
///
/// This method removes any caller-supplied `x-ts-ec` before proxying.
/// Response-side cookie mutation is centralized in EC finalize.
#[allow(clippy::too_many_arguments)]
#[must_use]
pub async fn handle_proxy(
&self,
method: &Method,
path: &str,
settings: &Settings,
kv: Option<&KvIdentityGraph>,
ec_context: &mut EcContext,
mut req: Request,
input: ProxyDispatchInput<'_>,
) -> Option<Result<Response, Report<TrustedServerError>>> {
let ProxyDispatchInput {
method,
path,
settings,
kv,
ec_context,
mut req,
} = input;
if let Some((proxy, _)) = self.find_route(method, path) {
// Organic proxy handler: generate if needed (best effort).
// Only generate for document navigations β€” subresource requests
Expand Down Expand Up @@ -1295,14 +1311,14 @@ mod tests {
EcContext::read_from_request(&settings, &req).expect("should read EC context");

// Call handle_proxy (uses futures executor in test environment)
let result = futures::executor::block_on(registry.handle_proxy(
&Method::GET,
"/integrations/test/ec",
&settings,
None,
&mut ec_context,
let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput {
method: &Method::GET,
path: "/integrations/test/ec",
settings: &settings,
kv: None,
ec_context: &mut ec_context,
req,
));
}));

// Should have matched and returned a response
assert!(result.is_some(), "should find route and handle request");
Expand Down Expand Up @@ -1335,14 +1351,14 @@ mod tests {
let mut ec_context =
EcContext::read_from_request(&settings, &req).expect("should read EC context");

let result = futures::executor::block_on(registry.handle_proxy(
&Method::GET,
"/integrations/test/ec",
&settings,
None,
&mut ec_context,
let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput {
method: &Method::GET,
path: "/integrations/test/ec",
settings: &settings,
kv: None,
ec_context: &mut ec_context,
req,
))
}))
.expect("should handle proxy request");

let response = result.expect("handler should succeed");
Expand Down Expand Up @@ -1372,14 +1388,14 @@ mod tests {
let mut ec_context =
EcContext::read_from_request(&settings, &req).expect("should read EC context");

let result = futures::executor::block_on(registry.handle_proxy(
&Method::GET,
"/integrations/test/ec",
&settings,
None,
&mut ec_context,
let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput {
method: &Method::GET,
path: "/integrations/test/ec",
settings: &settings,
kv: None,
ec_context: &mut ec_context,
req,
))
}))
.expect("should handle proxy request");

let response = result.expect("proxy handle should succeed");
Expand Down Expand Up @@ -1411,14 +1427,14 @@ mod tests {
let mut ec_context =
EcContext::read_from_request(&settings, &req).expect("should read EC context");

let result = futures::executor::block_on(registry.handle_proxy(
&Method::POST,
"/integrations/test/ec",
&settings,
None,
&mut ec_context,
let result = futures::executor::block_on(registry.handle_proxy(ProxyDispatchInput {
method: &Method::POST,
path: "/integrations/test/ec",
settings: &settings,
kv: None,
ec_context: &mut ec_context,
req,
));
}));

assert!(result.is_some(), "Should find POST route");
let response = result.unwrap();
Expand Down
4 changes: 0 additions & 4 deletions crates/trusted-server-core/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@
pub mod tests {
use crate::settings::Settings;

/// A well-formed synthetic ID for use in tests: 64 lowercase hex chars + `'.'` + 6 alphanumeric.
pub const VALID_SYNTHETIC_ID: &str =
"a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2.Ab12z9";

#[must_use]
pub fn crate_test_settings_str() -> String {
r#"
Expand Down
7 changes: 6 additions & 1 deletion docs/guide/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,15 @@ Returns EC identity plus the authenticated partner's UID and EID for the current
"eid": {
"source": "formally-vital-lion.edgecompute.app",
"uids": [{ "id": "mock-user-123", "atype": 3 }]
}
},
"cluster_size": 3
}
```

`uid`, `eid`, and `cluster_size` are optional and omitted when unavailable
(e.g. no partner UID synced yet, KV read degraded, or cluster size not
re-evaluated within the recheck window).

---

### POST /\_ts/api/v1/batch-sync
Expand Down
Loading