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
2 changes: 1 addition & 1 deletion .claude/commands/check-ci.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Run all CI checks locally, in order. Stop and report if any step fails.

1. `cargo fmt --all -- --check`
2. `cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare`
2. `cargo clippy-fastly && cargo clippy-axum && cargo clippy-cloudflare && cargo clippy-cloudflare-wasm && scripts/lint-wasm-clock.sh`
3. `cargo test-fastly && cargo test-axum && cargo test-cloudflare`
4. `cargo check-cloudflare` (wasm32-unknown-unknown target check, mirrors CI)
5. `cd crates/trusted-server-js/lib && npx vitest run`
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ jobs:
- name: Run cargo clippy (Cloudflare — wasm32-unknown-unknown)
run: cargo clippy-cloudflare-wasm

- name: Check wasm-safe clock usage in the Cloudflare build (wasm32-unknown-unknown)
run: scripts/lint-wasm-clock.sh

- name: Run cargo clippy (Spin — native)
run: cargo clippy-spin-native

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ Every PR must pass:
7. Docs format (`cd docs && npm run format`)
8. Markdown format outside `docs/` (requires `cd docs && npm ci` first): `docs/node_modules/.bin/prettier --config docs/.prettierrc --check "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md" "tinybird/**/*.md"`; fix with `--write` in place of `--check`
9. Native core build-digest test and lint (`cargo test -p trusted-server-core --test template_build_digest` and `cargo clippy -p trusted-server-core --test template_build_digest -- -D warnings`)
10. Wasm clock lint: `scripts/lint-wasm-clock.sh` (crates built into the Cloudflare Worker must use `web_time`, not the `std::time::{Instant,SystemTime}` `now`/`elapsed` methods, which panic there)

---

Expand Down
1 change: 1 addition & 0 deletions crates/trusted-server-adapter-cloudflare/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ build/
.worker-build/
.env.cloudflare.dev
.dev.vars
wrangler.integration.generated.toml
3 changes: 3 additions & 0 deletions crates/trusted-server-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ web-time = { workspace = true }
# need these — this block is intentionally scoped to `target_os = "unknown"` only.
getrandom = { workspace = true, features = ["js"] }
uuid = { workspace = true, features = ["js"] }
# `chrono::Utc::now()` calls `std::time::SystemTime::now()`, which panics on
# `wasm32-unknown-unknown`, unless `wasmbind` makes it read the JS clock.
chrono = { workspace = true, features = ["wasmbind"] }

[features]
default = []
Expand Down
7 changes: 6 additions & 1 deletion crates/trusted-server-core/src/auction/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
//! adapters provide the concrete sink implementation.

use std::collections::HashSet;
use std::time::Instant;

use chrono::Utc;
use error_stack::Report;
use serde::Serialize;
use uuid::Uuid;
use web_time::Instant;
Comment thread
dhruv8sh marked this conversation as resolved.

use crate::auction::orchestrator::OrchestrationResult;
use crate::auction::types::{AuctionRequest, AuctionResponse, Bid, BidStatus, MediaType};
Expand Down Expand Up @@ -211,6 +211,11 @@ impl AuctionObservationContext {
}

/// Return elapsed milliseconds since the observation was created.
///
/// On Cloudflare Workers, [`Instant`] reads `performance.now()`, which only
/// advances after I/O. There this counts time spent waiting on I/O and
/// reads 0 for CPU-only spans, so it is not comparable with the wall-clock
/// value other adapters report.
#[must_use]
pub fn elapsed_ms(&self) -> u64 {
u64::try_from(self.started_at.elapsed().as_millis()).unwrap_or(u64::MAX)
Expand Down
28 changes: 28 additions & 0 deletions crates/trusted-server-core/src/ec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,21 @@ pub(crate) fn checked_current_timestamp() -> Option<u64> {
.ok()
}

/// Returns the current time as a [`std::time::SystemTime`] without calling
/// `std::time::SystemTime::now()`, which panics on `wasm32-unknown-unknown`.
///
/// Use this for APIs that only accept the std type, such as `httpdate` or
/// chrono's `DateTime::from`. It reads the wasm-safe [`web_time::SystemTime`]
/// clock and adds the elapsed time to [`std::time::UNIX_EPOCH`], which is pure
/// arithmetic. On native and `wasm32-wasip1`, [`web_time`] delegates to the
/// std clock, so this equals `std::time::SystemTime::now()` there.
pub(crate) fn std_system_time_now() -> std::time::SystemTime {
std::time::UNIX_EPOCH
+ web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.unwrap_or_default()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1148,4 +1163,17 @@ mod tests {
"should return cookie value for revocation even when header is present"
);
}

#[test]
fn std_system_time_now_tracks_std_clock() {
let before = std::time::SystemTime::now();

let now = std_system_time_now();

let after = std::time::SystemTime::now();
assert!(
before <= now && now <= after,
"should fall between std clock reads taken before and after"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::str::FromStr;
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, Instant};
use std::time::Duration;

use error_stack::Report;
use regex::Regex;
use serde::Deserialize;
use web_time::Instant;

use crate::error::TrustedServerError;
use crate::platform::{RuntimeServices, StoreName};
Expand Down
12 changes: 2 additions & 10 deletions crates/trusted-server-core/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,16 +907,8 @@ fn apply_asset_origin_auth(
&config.region,
&credentials,
// s3_sigv4 converts this via chrono's `DateTime::<Utc>::from`, which
// only accepts `std::time::SystemTime`. `std::time::SystemTime::now()`
// panics on `wasm32-unknown-unknown` (Cloudflare Workers), so derive an
// equivalent `std::time::SystemTime` from the wasm-safe `web_time` clock:
// `UNIX_EPOCH + elapsed` is pure arithmetic and never calls the panicking
// `now()`. On Fastly (wasm32-wasip1) and native, `web_time` delegates to
// the std clock, so behavior is unchanged there.
std::time::UNIX_EPOCH
+ web_time::SystemTime::now()
.duration_since(web_time::UNIX_EPOCH)
.unwrap_or_default(),
// only accepts `std::time::SystemTime`.
crate::ec::std_system_time_now(),
)
}
}
Expand Down
7 changes: 5 additions & 2 deletions crates/trusted-server-core/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use std::borrow::Cow;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime};
use std::time::{Duration, SystemTime};

use brotli::Decompressor;
use brotli::enc::BrotliEncoderParams;
Expand All @@ -35,6 +35,7 @@ use flate2::write::{GzEncoder, ZlibEncoder};
use futures::StreamExt as _;
use http::{HeaderValue, Method, Request, Response, StatusCode, Uri, header};
use sha2::Digest as _;
use web_time::Instant;

use crate::auction::endpoints::{
merge_auction_eids, resolve_auction_eids, resolve_client_auction_eids,
Expand Down Expand Up @@ -6470,7 +6471,9 @@ fn origin_shared_ttl(
headers: &edgezero_core::http::HeaderMap,
max_age: Duration,
) -> Result<Duration, TemplateCacheBypassReason> {
origin_shared_ttl_at(headers, SystemTime::now(), max_age)
// `httpdate` parses `Date`/`Expires` into `std::time::SystemTime`, so take
// the wasm-safe std value rather than calling `SystemTime::now()`.
origin_shared_ttl_at(headers, crate::ec::std_system_time_now(), max_age)
}

fn origin_shared_ttl_at(
Expand Down
50 changes: 48 additions & 2 deletions crates/trusted-server-integration-tests/tests/common/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,28 @@ const GENERATED_AT: &str = "2026-06-23T00:00:00Z";
const APP_CONFIG: &str = include_str!("../../fixtures/configs/trusted-server.integration.toml");

pub fn integration_app_config_envelope(origin_port: u16) -> TestResult<String> {
app_config_envelope(origin_port, |_| Ok(()))
}

/// Builds the integration app-config envelope after `customize` edits the
/// parsed fixture.
fn app_config_envelope(
origin_port: u16,
customize: impl FnOnce(&mut toml::Table) -> TestResult<()>,
) -> TestResult<String> {
let origin_url = format!("http://127.0.0.1:{origin_port}");
let app_config: TrustedServerAppConfig = toml::from_str(APP_CONFIG).map_err(|error| {
let mut table: toml::Table = toml::from_str(APP_CONFIG).map_err(|error| {
Report::new(TestError::ConfigGeneration).attach(format!(
"invalid Trusted Server integration config: {error}"
))
})?;
customize(&mut table)?;
let app_config: TrustedServerAppConfig =
toml::Value::Table(table).try_into().map_err(|error| {
Report::new(TestError::ConfigGeneration).attach(format!(
"invalid customized Trusted Server integration config: {error}"
))
})?;
let mut settings = app_config.into_settings();
settings.publisher.origin_url = origin_url;
let app_config = TrustedServerAppConfig::new(settings).map_err(|report| {
Expand All @@ -36,7 +52,37 @@ pub fn integration_app_config_envelope(origin_port: u16) -> TestResult<String> {

pub fn cloudflare_config_json(origin_port: u16) -> TestResult<String> {
let envelope = integration_app_config_envelope(origin_port)?;
serde_json::to_string(&serde_json::json!({ CONFIG_BLOB_KEY: envelope })).map_err(|error| {
cloudflare_binding_json(&serde_json::json!({ CONFIG_BLOB_KEY: envelope }))
}

/// Builds a Cloudflare config binding with server-side auctions enabled.
///
/// The fixture's providers are removed so the enabled auction completes
/// through the orchestrator and telemetry without any outbound provider call.
pub fn cloudflare_auction_config_json(origin_port: u16) -> TestResult<String> {
let envelope = app_config_envelope(origin_port, |table| {
let auction = table_mut(table, "auction")?;
auction.insert("enabled".to_string(), toml::Value::Boolean(true));
auction.remove("providers");
auction.remove("bidders");
Ok(())
})?;
cloudflare_binding_json(&serde_json::json!({ CONFIG_BLOB_KEY: envelope }))
}

fn table_mut<'a>(table: &'a mut toml::Table, key: &str) -> TestResult<&'a mut toml::Table> {
table
.get_mut(key)
.and_then(toml::Value::as_table_mut)
.ok_or_else(|| {
Report::new(TestError::ConfigGeneration).attach(format!(
"integration config should contain a `[{key}]` table"
))
})
}

fn cloudflare_binding_json(binding: &serde_json::Value) -> TestResult<String> {
serde_json::to_string(binding).map_err(|error| {
Report::new(TestError::ConfigGeneration).attach(format!(
"failed to serialize Cloudflare config binding: {error}"
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,15 @@ const CI_CONFIG_TEMPLATE: &str = "wrangler.ci.toml";
const GENERATED_CI_CONFIG: &str = "wrangler.integration.generated.toml";
const TRUSTED_SERVER_CONFIG_PLACEHOLDER: &str = "TRUSTED_SERVER_CONFIG = \"{}\"";

fn write_generated_ci_config(wrangler_dir: &Path) -> TestResult<String> {
fn write_generated_ci_config(wrangler_dir: &Path, config_json: &str) -> TestResult<String> {
let template_path = wrangler_dir.join(CI_CONFIG_TEMPLATE);
let template = std::fs::read_to_string(&template_path)
.change_context(TestError::RuntimeSpawn)
.attach(format!(
"failed to read Cloudflare CI wrangler config at {}",
template_path.display()
))?;
let config_json = cloudflare_config_json(origin_port())?;
let generated = inject_cloudflare_config(&template, &config_json)?;
let generated = inject_cloudflare_config(&template, config_json)?;
let output_path = wrangler_dir.join(GENERATED_CI_CONFIG);
std::fs::write(&output_path, generated)
.change_context(TestError::RuntimeSpawn)
Expand Down Expand Up @@ -69,11 +68,37 @@ impl RuntimeEnvironment for CloudflareWorkers {
fn spawn(&self, _wasm_path: &Path) -> TestResult<RuntimeProcess> {
let wrangler_dir = self.wrangler_dir();
let config = if std::env::var("CI").is_ok() {
write_generated_ci_config(&wrangler_dir)?
write_generated_ci_config(&wrangler_dir, &cloudflare_config_json(origin_port())?)?
} else {
"wrangler.toml".to_string()
};
self.spawn_wrangler(&wrangler_dir, &config)
}

fn health_check_path(&self) -> &str {
"/.well-known/trusted-server.json"
}
}

impl CloudflareWorkers {
/// Start `wrangler dev` with a test-specific `TRUSTED_SERVER_CONFIG` binding.
///
/// Unlike [`RuntimeEnvironment::spawn`], this always renders the CI
/// wrangler template, so tests can enable features the shared fixture
/// leaves off without changing the other runtimes' configuration.
///
/// # Errors
///
/// Returns [`TestError::RuntimeSpawn`] if the wrangler config cannot be
/// written or `wrangler dev` cannot be started, and
/// [`TestError::RuntimeNotReady`] if the health check times out.
pub fn spawn_with_config_json(&self, config_json: &str) -> TestResult<RuntimeProcess> {
let wrangler_dir = self.wrangler_dir();
let config = write_generated_ci_config(&wrangler_dir, config_json)?;
self.spawn_wrangler(&wrangler_dir, &config)
}

fn spawn_wrangler(&self, wrangler_dir: &Path, config: &str) -> TestResult<RuntimeProcess> {
let port = super::find_available_port().unwrap_or(CLOUDFLARE_DEFAULT_PORT);

#[cfg(unix)]
Expand All @@ -83,13 +108,13 @@ impl RuntimeEnvironment for CloudflareWorkers {
.args([
"dev",
"--config",
config.as_str(),
config,
"--port",
&port.to_string(),
"--ip",
"127.0.0.1",
])
.current_dir(&wrangler_dir)
.current_dir(wrangler_dir)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.process_group(0)
Expand All @@ -108,13 +133,13 @@ impl RuntimeEnvironment for CloudflareWorkers {
.args([
"dev",
"--config",
config.as_str(),
config,
"--port",
&port.to_string(),
"--ip",
"127.0.0.1",
])
.current_dir(&wrangler_dir)
.current_dir(wrangler_dir)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
Expand Down Expand Up @@ -149,12 +174,6 @@ impl RuntimeEnvironment for CloudflareWorkers {
})
}

fn health_check_path(&self) -> &str {
"/.well-known/trusted-server.json"
}
}

impl CloudflareWorkers {
/// Resolve the Cloudflare adapter crate root.
///
/// Respects `CLOUDFLARE_WRANGLER_DIR` for CI overrides; falls back to
Expand Down
Loading
Loading