Skip to content

Fetch pages and assets through a source module - #1380

Open
jwrosewell wants to merge 40 commits into
feature/38-middleware-settingsfrom
feature/39-source-modules
Open

jwrosewell wants to merge 40 commits into
feature/38-middleware-settingsfrom
feature/39-source-modules

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 19 files, 2686 lines added, 930 removed
docs/guide 6 files, 370 lines added, 186 removed
docs/superpowers 1 file, 172 lines added, 0 removed
crates/trusted-server-adapter-fastly/src 5 files, 75 lines added, 24 removed
crates/trusted-server-adapter-cloudflare/src 2 files, 36 lines added, 6 removed
trusted-server.example.toml 1 file, 34 lines added, 16 removed
crates/trusted-server-adapter-axum/tests 2 files, 30 lines added, 5 removed
crates/trusted-server-adapter-cloudflare/tests 1 file, 30 lines added, 5 removed
crates/trusted-server-adapter-spin/src 2 files, 24 lines added, 4 removed
crates/trusted-server-adapter-axum/src 2 files, 18 lines added, 3 removed
crates/trusted-server-adapter-spin/tests 1 file, 18 lines added, 3 removed
crates/trusted-server-integration-tests/tests 3 files, 13 lines added, 3 removed
crates/tag/google-tag-manager 1 file, 12 lines added, 2 removed
crates/testing/seam-probe 1 file, 12 lines added, 2 removed
CHANGELOG.md 1 file, 11 lines added, 0 removed
crates/trusted-server-integration-tests/src 1 file, 10 lines added, 2 removed
crates/trusted-server-cli/src 1 file, 7 lines added, 4 removed
crates/auction/prebid-server 1 file, 6 lines added, 1 removed
crates/auction/prebid 1 file, 6 lines added, 1 removed
crates/trusted-server-integration-tests/fixtures 1 file, 6 lines added, 1 removed
.github/actions 1 file, 3 lines added, 3 removed
crates/cmp/inmobi 1 file, 3 lines added, 1 removed
crates/testing/testlight 1 file, 3 lines added, 0 removed
crates/trusted-server-cli/tests 2 files, 2 lines added, 2 removed
scripts/template-cache-local-test.sh 1 file, 2 lines added, 2 removed
.github/workflows 1 file, 1 line added, 1 removed
scripts/attestation-local-test.sh 1 file, 1 line added, 1 removed
scripts/config-by-host-local-test.sh 1 file, 1 line added, 1 removed
scripts/integration-tests-browser.sh 1 file, 1 line added, 1 removed
scripts/integration-tests.sh 1 file, 1 line added, 1 removed
scripts/smoke-common.sh 1 file, 1 line added, 1 removed

Closes

Closes #1332

Spec

Spec: source-modules

Test plan

CI on this pull request at ade271c passed all 29 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at ade271c

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

On top of the middleware settings table (#1379). Issue: #1332.

What this adds

What changes.

A source module says where the publisher path and the asset routes fetch from. [source] selects the publisher's source by name, and each [[proxy.asset_routes]] entry selects its own in a source table of the same shape. Core supplies origin, an HTTP origin, and a module a build links offers another through with_source_module.

[publisher]
domain = "www.example.com"
cookie_domain = "example.com"

[source]
module = "origin"

[source.origin]
url = "https://origin.example.com"
host_header = "www.example.com"
directory_index = "index.html"
auth = { type = "s3_sigv4", region = "eu-west-1" }

How it works.

  • One answer per request. A source answers a SourceLocation for each request, being the address and folder, the Host to send, the signing and the index file. The publisher path and the asset routes fetch, cache, rewrite the origin host and map its addresses in response headers from that answer, so a source that answers a different origin for each request is kept apart everywhere.
  • [publisher] keeps who the publisher is. The origin fields leave it for [source.origin], where host_header, directory_index and auth are optional. A document writing one where it was is refused naming the new place, and a document with no [source] is refused with the shape to write.
  • An asset route says where its content comes from the same way. [proxy.asset_routes.source] and the source's own table replace a route's own origin address and signing. A path in a route's url names a folder the target path is added under, a target path with a dot segment under a folder is answered 400, and a route applies directory_index as the publisher does.
  • A module sees the prefix request addressing removed. Request addressing (Address requests by path before choosing settings #1376) takes a prefix off the path and keeps it as a request extension, and a module read the continuing path only, so a module naming things after the tenant a path addressed could not tell which one it was. ModuleRequest::of now reads the removed prefix, removed_prefix() gives it, and a held request keeps it.
  • Stored pages and the publisher's stored assets are fetched again once. The origin part of every stored key keeps the form the old settings gave, but the key of a stored page and of a stored publisher asset also holds a code worked out from the settings as written out and from core's source. This series changes both, so each of those is fetched from the origin again on its first request after the upgrade. The key of a copy held for an asset route holds no such code, so those copies keep their keys. The configuration page masks the origin's url and host header by default, and the signing secrets are declared at their new paths.

Why

Where content comes from was written twice, in [publisher] and in every asset route, and fetched by code that knew one kind of source. A deployment that wants a page from assets built into its binary, from a store its platform offers, or from a service of its own had no place for that code but core. One interface core declares, with the origin fetch as the source core supplies, gives each of those a crate of its own and keeps one table for who the publisher is and one for where the content comes from.

What breaks

A [publisher] table carrying origin_url, origin_host_header_override, origin_auth, directory_index or the origin query rule is refused at startup, with the [source] shape to write instead, and an asset route carrying its own origin address or signing is refused the same way. Every deployment's settings document changes shape, so the build and the documents go out together. The integration test environment and the attestation script set TRUSTED_SERVER__SOURCE__ORIGIN__URL in place of the previous variable, because the overlay ignores a variable for a key the document lacks and the old name would silently leave a build on the placeholder origin. Every stored page and every stored asset of the publisher is fetched from the origin again on its first request after the upgrade, because their keys hold a code worked out from the settings, which now write out a [source] section where [publisher] held the origin settings. An [inspect] show or hide pattern that names a moved path, such as publisher.origin_url, matches no value in the new settings and is refused until it names the new path.

The commits

  1. A source module says where the publisher path and asset routes fetch from.
  2. A module sees the prefix request addressing removed.
  3. The local test scripts set the origin's address under [source.origin], through TRUSTED_SERVER__SOURCE__ORIGIN__URL, because the settings overlay ignores a variable for a key the document lacks and the scripts' old variable left the test harnesses on the placeholder origin, which the settings refuse.
  4. A page names itself by the Host its source sends. Where a source fetches through another address and sends the publisher's own name as Host, core now hands the page pipeline and every middleware that name as the page's origin host, so the page's own links are rewritten under the request's host. Where a source sends no other Host the two names are the same and nothing changes.
  5. An integration route is handed its request's evidence. handle_proxy takes the client address, the headers after the caller's Edge Cookie header is removed and the address asked for, so a route that asks for the request as evidence is no longer refused and answered 500.

The other commits are two test fixes (d6d0da0 and 878941c), the design page and its review (6eabcc6 and 0f51b33), and merges of the series below. 315cdf0 gives the header rewrite merged in main (#1238) the source settings' names, in its warnings, its handler tests and the guide.

The design page is docs/superpowers/specs/2026-10-14-source-modules-design.md, and the same page goes to the design documents pull request (#1342) so every design page stays in one place.

What is not in this pull request

A source other than the HTTP origin. The interface and the selection are here, a source that answers another address is a module of its own, and a source that holds its content itself, with no network origin, is a later extension of the interface.

…from

Where content comes from was written twice, in [publisher] and in every
asset route, and fetched by code that knew one kind of source. A source
module now answers it. [source] selects the publisher's, and each
[[proxy.asset_routes]] entry selects its own in a source table of the
same shape. Core supplies origin, an HTTP origin, and a module a build
links offers another through with_source_module.

For each request a source answers a SourceLocation, being the address
and folder, the Host to send, the signing and the index file. The
publisher path and the asset routes fetch, cache, rewrite the origin
host and follow the redirect rule from that answer, so a source that
answers a different origin for each request is kept apart everywhere.

The origin fields move into [source.origin] and
[proxy.asset_routes.source.origin]. A document writing one where it was
is refused naming the new place, and a document with no [source] is
refused. A path in an asset route's url names a folder the target path
is added under, a target path with a dot segment under a folder is
answered 400, and a route applies directory_index as the publisher does.
Stored page and asset keys are unchanged for an existing deployment. The
configuration page masks the origin's url and host_header by default,
and the signing secrets are declared at their new paths.

Design: docs/superpowers/specs/2026-10-14-source-modules-design.md
Request addressing takes a prefix off the path and keeps it as a request
extension, so a source can say where content comes from. A module was
handed the continuing path only, so a module that names things after the
tenant a path addressed, such as a prefix module naming cookies after the
publisher, could not tell which one it was.

ModuleRequest::of now reads the removed prefix and removed_prefix() gives
it, with_removed_prefix sets it, and ResolvedRequest keeps it, so a held
request still has it. A request addressing nothing carries none.
A route reads its request through its module call, and the call carried the
request as resolved, the settings and the request state, but no evidence. A
route that asks for the request as evidence, the client address and headers a
module reads, was refused with "there is no request evidence where this
module runs", so every such route answered 500 in production while its own
tests, which build the context by hand, passed.

handle_proxy now takes the evidence before the route takes the request, in
the form the permission path reads it: the normalized client address, the
headers after the caller's EC header is removed, and the address asked for.
The test goes through handle_proxy, so it fails if the evidence is dropped
again.

(cherry picked from commit efad1ece83c3ec214e7d60c3e0754fb1053c5b41)
A document without [source] is refused since the source series, and the
test built its registry from default settings.
The source series moved the origin's address out of [publisher], and the
overlay ignores a variable for a key the document lacks, so the scripts
that still set TRUSTED_SERVER__PUBLISHER__ORIGIN_URL left the template
cache harness, the config by host harness and the integration runs on
the placeholder origin, which the settings refuse.
A source may fetch through another host and send the publisher's own
name as the Host, as for an origin behind a proxy at a different
address. The page then names that Host's name and never the address's
host. Core looked for the address's host, which the page does not
carry, so it rewrote none of the page's own addresses, and every
middleware was told the proxy's host as the page's origin.

Core now hands the page pipeline and every middleware the Host the
source sends as the page's origin host, for a page fetched for the
request and for one served from the store alike. Where a source sends
no other Host the two names are the same, so nothing else changes. The
fetch itself still goes to the source's address.

Checked: core's library tests pass natively, including a new test that
fetches a page through another address and finds its own links pointed
at the request's host, and cargo test-axum, test-cloudflare, test-spin
and test-fastly pass. Clippy on core with -D warnings is clean.
…nges, and its limits

Review round 1 found that the keys of stored pages and of the publisher's
assets change with this series, that a page and every middleware are now
told the Host the source sends as the page's origin host, and that the
behavior the series adds to asset routes and to modules was missing from
the design. The design now says all three, adds configuration, adapters,
consent and privacy, failure handling and rollout, lists the paths with no
test, records the spelling of core's source as a known gap and records the
review round. The changelog entry no longer says stored keys are unchanged
and now states the host change.
Round 2 approved the design with no blocking issues. It could not confirm
that the per-request source test keeps no stored copies, and that holds.
The test runs in the default assembly mode, which stores no page, with
test services that select no cache, which store no asset, and their
template cache is the unavailable one. The design names no design
documents pull request, so no reference changed. The review log and the
revision record carry the round.
The origin redirect guide sentences on redirect loops and a removed path prefix move to the Redirects paragraph under [source.origin], where this series holds the origin settings.
@jwrosewell
jwrosewell added this pull request to stack #1401 October 15, 2026 17:56
@jwrosewell
jwrosewell removed this pull request from stack #1401 October 15, 2026 20:51
This branch moves the origin's address from [publisher] origin_url to
[source.origin] url, and the local test scripts set it there. The config
push tests from the config by host change still set
TRUSTED_SERVER__PUBLISHER__ORIGIN_URL, so every push they ran was refused
for the example's insecure default origin. They now set
TRUSTED_SERVER__SOURCE__ORIGIN__URL, as config_store_defaults.rs does.
The test of the InMobi module's CMP id range gave its settings a real
origin by writing the publisher table's origin_url, which this branch
moves to [source.origin] and no longer exposes, so the module's tests did
not compile from here up. The test now sets the origin through
Settings::set_origin_source, as the other tests here do.
The publisher's origin has moved from `[publisher] origin_url` to
`[source.origin] url`, and settings refuse it in the old place. The Axum
and Fastly tests of `/robots.txt` through the publisher path and the
Cloudflare test of the attestation pages still wrote it in `[publisher]`,
so each now selects the origin module in `[source]` and names the
address there, as the other tests do.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell jwrosewell closed this Oct 18, 2026
@jwrosewell jwrosewell reopened this Oct 18, 2026
handle_asset_proxy_request takes the route's source since sources became
modules, and the test added with the failed load fix called it without
one, so the core library tests did not compile. The test now passes
origin_source_of(&route), as the other asset route tests do.
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
The template cache key takes main's request_path_and_query, the reader's own
path and query, and keeps this row's origin identity from the source
module's location.
# Conflicts:
#	CHANGELOG.md
#	docs/guide/configuration.md
The header rewrite merged in main (#1238) names publisher.origin_url and publisher.origin_host_header_override in two warnings, a doc comment and the configuration guide, and its handler tests set those fields. This change moves both settings to source.origin.url and source.origin.host_header, so the warnings, the comment and the guide name the new settings and the tests set them through edit_origin, as the change's own tests do. Without the test change core's tests do not compile here.

cargo test -p trusted-server-core --lib -- redirect response_header_rewrite passes 58 tests.
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell jwrosewell closed this Oct 19, 2026
@jwrosewell jwrosewell reopened this Oct 19, 2026
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell jwrosewell changed the title A source module says where the publisher path and asset routes fetch from Fetch pages and assets through a source module Oct 19, 2026
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The publisher's response can only come from an origin fetch

1 participant