Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
12301a6
A build reports when and from what it was made
jwrosewell Oct 9, 2026
4edaec8
Signed evidence of who operates a deployment and which build it runs
jwrosewell Oct 9, 2026
94ae6a2
Every adapter serves the attestation pages
jwrosewell Oct 9, 2026
dffb3b2
The attestation guide, for an operator, a relying party and a reviewer
jwrosewell Oct 9, 2026
896942c
What a key schedule may hold is tested
jwrosewell Oct 9, 2026
a4cebd8
A harness checks the evidence as a verifier would
jwrosewell Oct 9, 2026
1060456
The permissions inspector's lock file takes the two signing crates
jwrosewell Oct 9, 2026
5ec0217
Merge branch 'review/11-config-by-host' into review/12-attestation
Jamesr51d Oct 12, 2026
96d8caf
A key schedule refuses a repeated key id or start
Jamesr51d Oct 12, 2026
7308239
Merge the data page sandbox from the inspection endpoints into attest…
Jamesr51d Oct 12, 2026
a656298
The attestation evidence names the tag the build was made from
jwrosewell Oct 13, 2026
b76c010
Merge split/11-config-by-host into split/12-attestation
Jamesr51d Oct 13, 2026
196da17
Merge split/11-config-by-host into split/12-attestation
Jamesr51d Oct 13, 2026
59e09b6
Allow the host clock in the build script's build time
Jamesr51d Oct 13, 2026
7ff40de
Sign no attestation evidence when the clock cannot be read
jwrosewell Oct 16, 2026
898a768
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 16, 2026
275862c
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 16, 2026
39db81e
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 16, 2026
e31dc94
Stop the build when an attestation key cannot sign
jwrosewell Oct 16, 2026
74dcbde
Answer the attestation pages on Cloudflare without the request preparers
jwrosewell Oct 16, 2026
6949260
Describe the clock answer and the key check in the attestation guide
jwrosewell Oct 16, 2026
ec365eb
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 16, 2026
f7b188b
Merge the review fixes into split/12-attestation
jwrosewell Oct 16, 2026
79334ff
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 16, 2026
e5e7fbf
The attestation guide says how a Cloudflare or Spin build gets its keys
jwrosewell Oct 17, 2026
af07343
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 18, 2026
94c8d43
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 18, 2026
6347fdd
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 18, 2026
b444626
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 18, 2026
e2c0246
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 19, 2026
519a949
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 19, 2026
5cb52a4
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 19, 2026
3774c5d
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 19, 2026
6e7ccd8
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
e1a7cb8
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
8226e2d
Take attestation out of the stack
jwrosewell Oct 28, 2026
fef96ce
Test that two routes on one method and path are refused
jwrosewell Oct 28, 2026
d2bdd3e
Give the seam probe a route outside /integrations/
jwrosewell Oct 28, 2026
2c7d996
Reach the probe's own path through Fastly and Axum
jwrosewell Oct 28, 2026
38befd4
Reach the probe's own path through Cloudflare and Spin
jwrosewell Oct 28, 2026
4158f9b
Say how a module serves an endpoint of its own
jwrosewell Oct 28, 2026
a9b4143
Add the module endpoints design
jwrosewell Oct 28, 2026
9dd6adc
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
972c73a
The endpoint proof's Cloudflare and Spin settings carry the sample po…
jwrosewell Oct 28, 2026
24083b1
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
8c901d2
Row 12 takes the final module endpoints design
jwrosewell Oct 28, 2026
210f4cb
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
6d9fdb4
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 2026
1167064
Row 12's design copy carries the date it is proposed, 2026-10-27
jwrosewell Oct 28, 2026
acd7975
Merge split/11-config-by-host into split/12-attestation
jwrosewell Oct 28, 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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 80 additions & 14 deletions crates/testing/seam-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
//! reports internal request state over an unauthenticated route, serves a
//! browser module that does nothing useful, and resolves location from static
//! configuration rather than from the request. It is a development dependency
//! of the Axum and Fastly adapters only, so no adapter's production build
//! reaches it.
//! of the four adapters only, so no adapter's production build reaches it.

use std::collections::BTreeMap;
use std::net::IpAddr;
Expand All @@ -27,7 +26,7 @@ use async_trait::async_trait;
use edgezero_core::body::Body as EdgeBody;
use error_stack::Report;
use http::header::{self, HeaderValue};
use http::{Request, Response};
use http::{Method, Request, Response};
use serde::Deserialize;
use serde_json::json;
use trusted_server_core::auction::demand::{
Expand Down Expand Up @@ -79,6 +78,13 @@ pub const SEAM_PROBE_REPORT_PATH: &str = "/integrations/seam_probe/report";
/// Path suffix of the report route, relative to the proxy prefix.
const REPORT_ROUTE_SUFFIX: &str = "/report";

/// Path of the probe's route outside `/integrations/`, which the proxy
/// declares as written rather than beneath its prefix.
pub const SEAM_PROBE_OWN_PATH: &str = "/seam-probe/own-path";

/// Body the probe answers a `GET` of [`SEAM_PROBE_OWN_PATH`] with.
pub const SEAM_PROBE_OWN_BODY: &str = "answered by the seam probe";

/// The browser module this registration carries, built outside
/// `trusted-server-js`.
pub const PROBE_JS: &str = include_str!("../js/probe.js");
Expand All @@ -97,7 +103,8 @@ pub const PROBE_JS_SHA256: &str =
pub const SEAM_PROBE_COUNTRY_MESSAGE: &str =
"`[testing.seam-probe] country` must be exactly two letters";

/// Request header a caller sets to have the preparer count that request.
/// Request header a caller sets to have the preparer, and the handler of
/// [`SEAM_PROBE_OWN_PATH`], count that request.
///
/// The header's value is a token the caller chooses, so tests running in
/// parallel count into separate entries.
Expand All @@ -112,6 +119,11 @@ pub const SEAM_PROBE_COUNT_HEADER: &str = "x-seam-probe-count";
static PREPARE_COUNTS: LazyLock<Mutex<BTreeMap<String, usize>>> =
LazyLock::new(|| Mutex::new(BTreeMap::new()));

/// How many times the handler of [`SEAM_PROBE_OWN_PATH`] ran for each
/// counting token, so a caller can tell that a request never reached it.
static OWN_PATH_COUNTS: LazyLock<Mutex<BTreeMap<String, usize>>> =
LazyLock::new(|| Mutex::new(BTreeMap::new()));

/// How many times the preparer ran for requests carrying `token` in
/// [`SEAM_PROBE_COUNT_HEADER`].
///
Expand All @@ -120,26 +132,46 @@ static PREPARE_COUNTS: LazyLock<Mutex<BTreeMap<String, usize>>> =
/// Panics when the counter's lock is poisoned.
#[must_use]
pub fn prepare_runs_for(token: &str) -> usize {
PREPARE_COUNTS
runs_for(&PREPARE_COUNTS, token)
}

/// How many times the handler of [`SEAM_PROBE_OWN_PATH`] ran for requests
/// carrying `token` in [`SEAM_PROBE_COUNT_HEADER`].
///
/// # Panics
///
/// Panics when the counter's lock is poisoned.
#[must_use]
pub fn own_path_runs_for(token: &str) -> usize {
runs_for(&OWN_PATH_COUNTS, token)
}

/// The runs `counts` holds for `token`.
fn runs_for(counts: &Mutex<BTreeMap<String, usize>>, token: &str) -> usize {
counts
.lock()
.expect("should lock the seam probe prepare counts")
.expect("should lock a seam probe counter")
.get(token)
.copied()
.unwrap_or(0)
}

/// Records one preparer run against the token the request carries, if any.
fn count_prepare_run(request: &Request<EdgeBody>) {
count_run(&PREPARE_COUNTS, request);
}

/// Records one run in `counts` against the token the request carries, if
/// any.
fn count_run(counts: &Mutex<BTreeMap<String, usize>>, request: &Request<EdgeBody>) {
let Some(token) = request
.headers()
.get(SEAM_PROBE_COUNT_HEADER)
.and_then(|value| value.to_str().ok())
else {
return;
};
let mut counts = PREPARE_COUNTS
.lock()
.expect("should lock the seam probe prepare counts");
let mut counts = counts.lock().expect("should lock a seam probe counter");
*counts.entry(token.to_string()).or_insert(0) += 1;
}

Expand Down Expand Up @@ -557,6 +589,24 @@ impl SeamProbeProxy {
);
Ok(response)
}

/// Answers [`SEAM_PROBE_OWN_PATH`] and counts the run against the
/// request's token. A `HEAD` answer carries the headers a `GET` answer
/// carries and no body, which the handler leaves out itself because
/// nothing in core, the adapters or `EdgeZero` strips one.
fn own_path(&self, req: &Request<EdgeBody>) -> Response<EdgeBody> {
count_run(&OWN_PATH_COUNTS, req);
let body = if req.method() == Method::HEAD {
EdgeBody::empty()
} else {
EdgeBody::from(SEAM_PROBE_OWN_BODY)
};
let mut response = Response::new(body);
response
.headers_mut()
.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain"));
response
}
}

#[async_trait(?Send)]
Expand All @@ -566,14 +616,21 @@ impl IntegrationProxy for SeamProbeProxy {
}

fn routes(&self) -> Vec<IntegrationEndpoint> {
vec![self.get(REPORT_ROUTE_SUFFIX)]
vec![
self.get(REPORT_ROUTE_SUFFIX),
IntegrationEndpoint::get(SEAM_PROBE_OWN_PATH),
IntegrationEndpoint::new(Method::HEAD, SEAM_PROBE_OWN_PATH),
]
}

async fn handle(
&self,
call: trusted_server_core::module_context::ModuleCall<'_>,
req: Request<EdgeBody>,
) -> Result<Response<EdgeBody>, Report<TrustedServerError>> {
if req.uri().path() == SEAM_PROBE_OWN_PATH {
return Ok(call.inject_with(self, &req, Self::own_path)?);
}
call.inject_with(self, req, Self::route)?.await
}
}
Expand Down Expand Up @@ -1019,12 +1076,21 @@ mod tests {

#[test]
fn report_route_is_the_documented_path() {
let routes = SeamProbeProxy.routes();
let routes: Vec<(Method, String)> = SeamProbeProxy
.routes()
.into_iter()
.map(|route| (route.method, route.path))
.collect();

assert_eq!(routes.len(), 1, "should register exactly one route");
assert_eq!(
routes[0].path, SEAM_PROBE_REPORT_PATH,
"should route the documented report path"
routes,
[
(Method::GET, SEAM_PROBE_REPORT_PATH.to_owned()),
(Method::GET, SEAM_PROBE_OWN_PATH.to_owned()),
(Method::HEAD, SEAM_PROBE_OWN_PATH.to_owned()),
],
"should route the documented report path, and the probe's own path for GET and \
HEAD"
);
}

Expand Down
Loading
Loading